Skip to main content

Add an API Module

How to add a new bounded context to the API (e.g., ingredient).

1. Create the Module Structure

apps/api/internal/modules/ingredient/
domain/
ingredient.go
application/
service.go
infra/
repository.go
migrations.go
migrations/
00001_create_schema.sql
00002_create_ingredients_table.sql
queries/
ingredients.sql
sqlc/
sqlc.yaml
db/ # generated by sqlc — do not edit
transport/
api/
openapi.yaml
oapi-codegen.yaml
handler.go
module.go

2. Write the First Migrations

Every module's first migration creates its schema. Create infra/migrations/00001_create_schema.sql:

-- +goose Up
CREATE SCHEMA IF NOT EXISTS ingredient;

-- +goose Down
DROP SCHEMA IF EXISTS ingredient CASCADE;

Then create tables in infra/migrations/00002_create_ingredients_table.sql:

-- +goose Up
CREATE TABLE ingredient.ingredients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- +goose Down
DROP TABLE IF EXISTS ingredient.ingredients;

3. Embed the Migrations

Create infra/migrations.go:

package infra

import "embed"

//go:embed migrations/*.sql
var Migrations embed.FS

4. Add sqlc Queries and Config

Create infra/queries/ingredients.sql with schema-qualified table names:

-- name: ListIngredients :many
SELECT id, name, created_at, updated_at
FROM ingredient.ingredients
ORDER BY name;

Create infra/sqlc/sqlc.yaml:

version: "2"
sql:
- engine: "postgresql"
queries: "../queries/"
schema: "../migrations/"
gen:
go:
package: "ingredientdb"
out: "../db"
sql_package: "pgx/v5"

Generate the code:

cd apps/api
go tool sqlc generate --file internal/modules/ingredient/infra/sqlc/sqlc.yaml

Add the sqlc target to the Makefile sqlc rule.

5. Write the Domain Layer

Define entities and the repository interface in domain/ingredient.go:

package domain

import (
"context"
"time"

"github.com/google/uuid"
)

type Ingredient struct {
ID uuid.UUID
Name string
CreatedAt time.Time
UpdatedAt time.Time
}

type IngredientRepository interface {
List(ctx context.Context) ([]Ingredient, error)
}

6. Write the OpenAPI Spec

Create transport/api/openapi.yaml with full paths (e.g., /ingredients). Reference shared types via $ref:

openapi: "3.0.3"
info:
title: Ingredient API
version: 0.1.0

tags:
- name: Ingredients

paths:
/ingredients:
get:
operationId: listIngredients
summary: List all ingredients
tags: [Ingredients]
responses:
"200":
description: A list of ingredients
content:
application/json:
schema:
$ref: "#/components/schemas/ListIngredientsResponse"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "../../../../shared/api/schemas.yaml#/components/schemas/Error"

components:
schemas:
# define module-specific schemas here

7. Add the oapi-codegen Config

Create transport/api/oapi-codegen.yaml:

package: api
output: server.gen.go
generate:
chi-server: true
models: true
import-mapping:
../../../../shared/api/schemas.yaml: github.com/marcuslindfeldt/chompardo/apps/api/internal/shared/api
compatibility:
apply-chi-middleware-first-to-last: true

8. Create the Handler

Create transport/api/handler.go with the go:generate directive and service dependency:

package api

//go:generate go tool oapi-codegen --config oapi-codegen.yaml openapi.yaml

import (
"encoding/json"
"log"
"net/http"

"github.com/marcuslindfeldt/chompardo/apps/api/internal/modules/ingredient/application"
)

type Server struct {
svc *application.IngredientService
}

func NewServer(svc *application.IngredientService) *Server {
return &Server{svc: svc}
}

func (s *Server) ListIngredients(w http.ResponseWriter, r *http.Request) {
// implementation
}

9. Create module.go

Wire up the module, expose migrations, and accept the shared pool:

package ingredient

import (
"io/fs"

"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/marcuslindfeldt/chompardo/apps/api/internal/modules/ingredient/application"
"github.com/marcuslindfeldt/chompardo/apps/api/internal/modules/ingredient/infra"
ingredientapi "github.com/marcuslindfeldt/chompardo/apps/api/internal/modules/ingredient/transport/api"
)

var Migrations fs.FS = infra.Migrations

func Register(r chi.Router, pool *pgxpool.Pool) {
repo := infra.NewIngredientRepository(pool)
svc := application.NewIngredientService(repo)
h := ingredientapi.NewServer(svc)
ingredientapi.HandlerFromMux(h, r)
}

10. Register in main.go

Add to cmd/api/main.go:

import "github.com/marcuslindfeldt/chompardo/apps/api/internal/modules/ingredient"

// In the migrationSources() function:
{Name: "ingredient", FS: ingredient.Migrations},

// In main(), after recipe.Register:
ingredient.Register(r, pool)

Do the same in cmd/migrate/main.go for the migration source.

11. Generate and Verify

pnpm generate --filter=api
go build ./...

The glob pattern internal/modules/*/transport/api/openapi.yaml picks up the new module spec automatically — no other config changes needed.