Skip to content
qube
Go back

Build a Modern REST API With Go and PostgreSQL — Part 1

Originally published on Medium on August 16, 2022.

REST API in Go with PostgreSQL and sqlc

Build a Modern REST API With Go Part 1

Photo by Sunder Muthukumaran on Unsplash

Photo by Sunder Muthukumaran on Unsplash

This article is the first in a series covering all the aspects of implementing a modern REST API microservice step by step:

  1. Defining a SQL First data model with sqlc
  2. Implementing the REST API with Gin
  3. Configuring with Viper
  4. Building and running in a container
  5. Containerized tests

All the code for the series is available at https://github.com/bquenin/go-modern-rest-api-tutorial

What is The Source of Truth for our Data Model?

There are usually two main approaches for synchronizing your database models and queries with your code:

Both approaches have their pros and cons. Code first is usually better when you want the ORM library to handle all the boilerplate CRUD operations for you. However, it could get in the way if you want to fine-tune your database queries. In this case, the SQL first approach may be more appropriate, allowing you to have complete control over your database schema and fine-tuning your SQL queries, which could be necessary depending on the type of APIs you are building. However, with this approach, you will have to write all the SQL queries yourself.

There is no right or wrong answer on which is better. It entirely depends on what you feel more comfortable with, as you can build production-grade applications using both. In this tutorial, we will focus on the SQL first approach and use sqlc to generate the code from our SQL schemas and queries.

SQL First Data Model

Defining the data model is a key step in implementing a REST API microservice. You must manipulate the right concepts and understand your consumer domain well.

However, this is beyond the scope of this tutorial, and we will focus solely on how to build a REST API microservice step by step. Therefore, we will use the same model as in the sqlc tutorial:

CREATE TABLE authors
(
    id   BIGSERIAL PRIMARY KEY,
    name VARCHAR(32) NOT NULL,
    bio  TEXT NOT NULL
);schema.sql
-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING *;

-- name: GetAuthor :one
SELECT *
FROM authors
WHERE id = $1
LIMIT 1;

-- name: UpdateAuthor :one
UPDATE authors
SET name = $2,
    bio  = $3
WHERE id = $1
RETURNING *;

-- name: PartialUpdateAuthor :one
UPDATE authors
SET name = CASE WHEN @update_name::boolean THEN @name::VARCHAR(32) ELSE name END,
    bio  = CASE WHEN @update_bio::boolean THEN @bio::TEXT ELSE bio END
WHERE id = @id
RETURNING *;

-- name: DeleteAuthor :exec
DELETE
FROM authors
WHERE id = $1;

-- name: ListAuthors :many
SELECT *
FROM authors
ORDER BY name;queries.sql

The schema.sql file describes a simple Author object, and the queries.sql file provides all the CRUDL (Create, Read, Update, Delete, and List) queries. As you can notice, each query is preceded by a comment providing the name of the function to be generated and how many results it returns (one of exec, one, and many). This example is trivial, but consult the sqlc documentation if your model is more complex.

The partial update SQL query is more complex as queries in sqlc must be fully structured in advance. Therefore, each field is conditionally updated based on the presence of an associated boolean. For instance, the name field is updated only if the update_name field is true.

Many thanks to Brandur for his nice article about sqlc and Postgres.

Generating the Go code

First, we need to configure sqlc:

version: "2"
sql:
- schema: "sql/schema.sql"
  queries: "sql/queries.sql"
  engine: "postgresql"
  gen:
    go:
      package: "database"
      out: "internal/database"sqlc.yaml

In this case, we are configuring sqlc to produce Postgres-compatible code. We’re also specifying the schema and queries files, output path, and package name for the generated code. Our project structure looks like this:

├── sql
│   ├── queries.sql
│   └── schema.sql
└── sqlc.yaml

We can now generate the code by running the following command:

sqlc generate

It generates the following files:

├── internal
│   └── database
│       ├── db.go
│       ├── models.go
│       └── queries.sql.go
├── sql
│   ├── queries.sql
│   └── schema.sql
└── sqlc.yaml
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.15.0

package database

import ()

type Author struct {
	ID   int64
	Name string
	Bio  string
}models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.15.0
// source: queries.sql

package database

import (
	"context"
)

const createAuthor = `-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING id, name, bio
`

type CreateAuthorParams struct {
	Name string
	Bio  string
}

func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
	row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const deleteAuthor = `-- name: DeleteAuthor :exec
DELETE
FROM authors
WHERE id = $1
`

func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
	_, err := q.db.ExecContext(ctx, deleteAuthor, id)
	return err
}

const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio
FROM authors
WHERE id = $1
LIMIT 1
`

func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
	row := q.db.QueryRowContext(ctx, getAuthor, id)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const listAuthors = `-- name: ListAuthors :many
SELECT id, name, bio
FROM authors
ORDER BY name
`

func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
	rows, err := q.db.QueryContext(ctx, listAuthors)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Author
	for rows.Next() {
		var i Author
		if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const partialUpdateAuthor = `-- name: PartialUpdateAuthor :one
UPDATE authors
SET name = CASE WHEN $1::boolean THEN $2::VARCHAR(32) ELSE name END,
    bio  = CASE WHEN $3::boolean THEN $4::TEXT ELSE bio END
WHERE id = $5
RETURNING id, name, bio
`

type PartialUpdateAuthorParams struct {
	UpdateName bool
	Name       string
	UpdateBio  bool
	Bio        string
	ID         int64
}

func (q *Queries) PartialUpdateAuthor(ctx context.Context, arg PartialUpdateAuthorParams) (Author, error) {
	row := q.db.QueryRowContext(ctx, partialUpdateAuthor,
		arg.UpdateName,
		arg.Name,
		arg.UpdateBio,
		arg.Bio,
		arg.ID,
	)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const truncateAuthor = `-- name: TruncateAuthor :exec
TRUNCATE authors
`

func (q *Queries) TruncateAuthor(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, truncateAuthor)
	return err
}

const updateAuthor = `-- name: UpdateAuthor :one
UPDATE authors
SET name = $2,
    bio  = $3
WHERE id = $1
RETURNING id, name, bio
`

type UpdateAuthorParams struct {
	ID   int64
	Name string
	Bio  string
}

func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (Author, error) {
	row := q.db.QueryRowContext(ctx, updateAuthor, arg.ID, arg.Name, arg.Bio)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}queries.sql.go

As you can see from the header, this code is not to be edited manually. If anything needs to be modified, it must be done in the schema.sql or queries.sql files. You can also change the sqlc.yaml configuration to tweak the code generation parameters.

Using the Generated Code

The code generated by sqlc relies on the Go standard library SQL abstraction. It means we need to use the appropriate Go SQL driver to instantiate a connection to the database. The following file does just this:

package database

// ...

type Postgres struct {
	DB *sql.DB
}

func NewPostgres(host, user, password string) (*Postgres, error) {
	db, err := sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s sslmode=disable", host, user, password))
	if err != nil {
		return nil, err
	}
	if err := db.Ping(); err != nil {
		return nil, err
	}
	return &Postgres{DB: db}, nil
}postgres.go

We can now instantiate our Postgres database:

package main

// ...

func main() {
	// Instantiates the database
	postgres, err := database.NewPostgres(/* credentials */)
	if err != nil {
		log.Fatal(err.Error())
	}
}main.go

What’s Next?

Now that we have completed our database implementation, we will have a look at how to implement our REST API.


Share this post:

Previous Post
Translate Any Retro Game on the Fly With Google Cloud AI and Go
Next Post
Modern REST API With Go and PostgreSQL — Part 2