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.