Skip to main content

API

Go modular monolith serving the Chompardo backend. Runs at http://127.0.0.1:4000 (proxied via https://main-api.chompardo.dev).

Project Structure

apps/api/
cmd/
api/ Entry point — wires modules, starts HTTP server
migrate/ Standalone migration CLI for production deployments
openapi.yaml Root API metadata (title, version) for spec bundling
internal/
database/ Shared DB connectivity (pool, migrations)
modules/ Bounded contexts (one folder per domain module)
recipe/ Example module
domain/ Entities, value objects, repository interfaces
application/ Use cases and services
infra/ Concrete implementations (repos, sqlc, migrations)
migrations/ Goose SQL migration files
queries/ sqlc-annotated SQL queries
sqlc/ sqlc config (sqlc.yaml)
db/ sqlc-generated code (do not edit)
transport/ Transport layer (HTTP, events, etc.)
api/ OpenAPI spec, codegen config, generated server code + handlers
module.go Registers module routes, exposes Migrations FS
shared/
api/ Shared schemas, bundled spec embed, generated types
auth/ JWT validation (JWKS, user context helpers)
middleware/ Cross-cutting middleware (CORS, logging, auth, recovery)
pkg/ Public utilities (importable by other Go modules)

Dependency Rule

Dependencies point inward: transportapplicationdomaininfra

  • domain defines interfaces, everything else implements or calls them
  • application depends only on domain interfaces
  • infra implements domain interfaces (e.g., Postgres repository)
  • transport calls application services to handle requests

OpenAPI-First Code Generation

The API uses an OpenAPI-first approach with oapi-codegen. The spec is the source of truth — you design the API contract first, then generate server code from it.

Pipeline

  1. Each module owns its OpenAPI spec at internal/modules/<name>/transport/api/openapi.yaml
  2. Module specs use full paths (e.g., /recipes, /recipes/{id}) and are self-contained
  3. Shared types (like Error) live in internal/shared/api/schemas.yaml and are referenced via $ref
  4. redocly join auto-discovers and merges all module specs into a single bundled spec
  5. oapi-codegen generates a ServerInterface and models from each module's spec
  6. The bundled spec is embedded via go:embed and served at GET /openapi.yaml
Module specs ──┐
├── redocly join ──→ bundled spec (embedded, served at /openapi.yaml)
Root metadata ─┘

Module spec ──→ oapi-codegen ──→ ServerInterface + models (per module)
Shared schemas ──→ oapi-codegen ──→ shared Go types (Error, etc.)

Safety Nets

  • Turborepo pipelinebuild depends on generate, so builds always regenerate first
  • CI check — runs pnpm generate and fails if git diff detects stale generated files

Authentication

The API validates JWT Bearer tokens issued by the Identity service. All module routes require authentication; operational endpoints (/healthz, /openapi.yaml) are public.

How it works

  1. Client sends Authorization: Bearer <token> (case-insensitive scheme per RFC 7235)
  2. RequireAuth middleware extracts the token and validates it against the Identity service's JWKS
  3. Validation checks: signature (EdDSA/Ed25519), issuer, audience, expiration, and sub claim
  4. On success, the authenticated user is injected into the request context
  5. Handlers retrieve the user via auth.UserFromContext(ctx)

Unauthenticated requests receive a 401 with a WWW-Authenticate: Bearer header per RFC 6750.

JWKS caching

Public keys are fetched from the Identity service's /api/auth/jwks endpoint at startup and cached with automatic background refresh. If a token arrives with an unknown kid, the library re-fetches the JWKS to handle key rotation.

The JWKS fetch is eager — the API will fail to start if the Identity service is unavailable. In container orchestration environments, ensure the Identity service is healthy before starting the API.

Middleware variants

MiddlewareBehavior
RequireAuthReturns 401 if no valid token is present
OptionalAuthPasses through without auth, but populates context if a valid token is present

Key files

FileResponsibility
internal/shared/auth/config.goAuth configuration (AUTH_JWKS_URL, etc.)
internal/shared/auth/jwks.goJWKS fetching, caching, and JWT validation
internal/shared/auth/context.goUser struct, UserFromContext, ContextWithUser
internal/shared/middleware/auth.goRequireAuth and OptionalAuth middleware

Database

Connection

The API connects to PostgreSQL via pgx v5 (pgxpool). A single connection pool is created at startup in main.go and injected into each module's Register() function.

Local Postgres database api is auto-created by infra/local/postgres/01-init.sh:

  • Role: api
  • Password: api_secret
  • Port: 5432

Schema-per-Module

Each module owns its own PostgreSQL schema (e.g., recipe module uses the recipe schema). This provides:

  • Namespace isolation — tables from different modules never collide
  • Clear ownership — a module's schema is defined and migrated alongside its code
  • No cross-module foreign keys — modules are bounded contexts and communicate through application-level interfaces, not database joins

All SQL uses schema-qualified table names (e.g., recipe.recipes) rather than relying on search_path. This is explicit and survives connection pool recycling.

Migrations

Migrations use goose v3 with embedded SQL files. Each module has its own:

  • Migration files in infra/migrations/ (embedded via //go:embed)
  • Goose version table named goose_<module>_version (e.g., goose_recipe_version)

Since each module tracks versions independently, migration numbering starts at 00001 per module — no global coordination required.

The first migration for every module creates its schema:

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

Query Generation

SQL queries use sqlc to generate type-safe Go code from annotated SQL. Each module has its own sqlc config at infra/sqlc/sqlc.yaml that generates code into infra/db/.

SQL queries (infra/queries/)
──→ sqlc generate
──→ type-safe Go code (infra/db/)

The generated Queries struct accepts any DBTX interface (pool or transaction), and the repository layer wraps it to map between pgtype types and clean domain types.