A structural reference for a small Go HTTP/RPC service backed by PostgreSQL. The goal is to show one opinionated layout for a production-style service — not to ship a real bookstore.
The service exposes two API styles from a single binary:
- ConnectRPC (over HTTP/2 with h2c) for
health.v1.HealthService - OpenAPI 3.1.1 / REST for the bookstore domain (
/books,/books/{id})
The domain is deliberately minimal (book / author / genre) — just enough to show joins and type conversions without drowning the structure in features.
- Repository layout — directory tree and what lives where
- Design decisions — why
Service(notRepository), package-by-feature, transport-agnostic wiring, and the layering rules that follow - Server lifecycle and graceful shutdown — signal
handling, the
concpool pattern, ordered teardown witherrors.Join, and how the sequence scales as resources are added - Getting started — full prerequisites,
oapi-codegen-exprationale, test layering, codegen targets - Adding a new entity — step-by-step walkthrough
- Extending this layout — where observability and other cross-cutting concerns slot in
You'll need Go 1.25+, just, and Docker
(or any reachable Postgres). For the full prerequisite list and rationale,
see Getting started.
just db-up # Postgres 17 on localhost:5433
just db-down # tear it downMigrations are embedded via //go:embed and applied automatically by
pgdb.NewPostgres on startup.
just runThat executes go run ./cmd/bookstore server --config etc/bookstore.yaml.
Migrations apply on first start, so the tables exist after the server is up.
just seedThe seed is idempotent (ON CONFLICT DO NOTHING).
ConnectRPC health check via buf curl:
buf curl \
--schema api/health/v1/health.proto \
--protocol connect \
--http2-prior-knowledge \
http://localhost:8080/health.v1.HealthService/CheckREST endpoints (matched against the seed data):
# Fetch a specific book with hydrated author + genre
curl http://localhost:8080/books/2942867b-3a18-4cae-99e1-2f2816110b07
# Substring search on title
curl 'http://localhost:8080/books?query=go'
# Filter by genre slug
curl 'http://localhost:8080/books?genre=fantasy&limit=10'just testDB-backed tests boot a throwaway Postgres via testcontainers-go and
auto-skip if Docker isn't reachable. See
Getting started for what each test
file demonstrates.
just generate # everything
just generate-api # buf generate (protobuf + Connect)
just generate-openapi # oapi-codegen
just generate-sql # sqlc generate