Simplifying Backend Logic with Code Generation in Go

Simplifying Backend Logic with Code Generation in Go

Backend development leans on repetitive boilerplate, especially around database queries and API servers. Writing that code by hand is slow and easy to get wrong. Codegen tools turn a declarative spec into type-safe Go code you do not maintain by hand.

This article walks through two tools:

Why Use Code Generation?

SQL Code Generation with sqlc

What is sqlc?

sqlc reads your .sql files and emits Go functions that run those queries with typed parameters and results.

How to Use sqlc

  1. Write the schema and the queries.
  2. Set input and output in sqlc.yaml.
  3. Run sqlc generate.

Example

Say you have a users table:

-- schema.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
);

And a query that looks up a user by email:

-- queries.sql
-- name: GetUserByEmail :one
SELECT id, name, email FROM users WHERE email = $1;

The sqlc.yaml config:

version: "1"
packages:
  - name: "db"
    path: "./db"
    queries: "./queries.sql"
    schema: "./schema.sql"
    engine: "postgresql"

Run:

sqlc generate

sqlc produces a method like:

func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error)

Use it in the backend:

user, err := dbQueries.GetUserByEmail(ctx, "alice@example.com")
if err != nil {
    // handle error
}
fmt.Println("User:", user.Name)

The manual SQL string handling and row scanning are gone. The code is cleaner and safer.

Server Code Generation with oapi-codegen

What is oapi-codegen?

oapi-codegen reads an OpenAPI spec and emits Go server and client code. It builds the interfaces and the request and response types. You are left to implement the business logic.

How to Use oapi-codegen

  1. Define the API in an OpenAPI YAML or JSON file.
  2. Run oapi-codegen.
  3. Implement the generated interface methods.

Example

Here is a small OpenAPI spec api.yaml:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users/{email}:
    get:
      summary: Get user by email
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: User found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "404":
          description: User not found
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string

Generate the server code:

oapi-codegen -generate types,server -package api -o api.gen.go api.yaml

This produces request and response types such as the User struct, plus a ServerInterface with a method:

GetUsersEmail(ctx context.Context, email string) (api.User, error)

Implement the interface:

type ServerImpl struct {
    db *db.Queries
}
 
func (s *ServerImpl) GetUsersEmail(ctx context.Context, email string) (api.User, error) {
    user, err := s.db.GetUserByEmail(ctx, email)
    if err != nil {
        return api.User{}, err
    }
    return api.User{
        Id:    int64(user.ID),
        Name:  user.Name,
        Email: user.Email,
    }, nil
}

Wire up the server:

router := api.NewRouter(&ServerImpl{db: dbQueries})
http.ListenAndServe(":8080", router)

Benefits of Combining sqlc and oapi-codegen

Conclusion

sqlc and oapi-codegen together automate the database access and the API server code in a Go backend. You write less boilerplate and hit fewer runtime errors. The focus stays on the core logic of the application.

© Melvin Laplanche - All rights reserved.