Build a production-grade REST API in Go, step by step
A hands-on series: start with under 200 lines of in-memory CRUD and end with Postgres, JWT auth, rate limiting, TLS and a full observability stack.

Most Go REST tutorials stop at "it returns JSON". Then you ship something like it, and the real work begins: what happens when the request body is 2 GB, when the database is slow, when a client hammers the login endpoint, when you need to figure out why 3% of requests are failing at 2 a.m.?
This series closes that gap. We build one small application, a users-and-orders API, and improve it one concern at a time. Every post adds a single idea, explains why it exists, shows the failure it prevents, and ends with the complete code for that step, so the post alone is enough to build the project.
By the end you will have built, understood, and tested:
- a clean
net/httpservice (no framework) with a layered structure - persistence: first SQLite, then PostgreSQL with versioned migrations and a tuned connection pool
- input validation, correct error handling and graceful shutdown
- structured logs, Prometheus metrics, distributed traces and health checks, wired into Grafana
- a Docker image (23 MB, non-root) and a full local environment with
docker compose - keyset pagination and the indexes that make it fast
- authentication (bcrypt, JWT access tokens, rotating refresh tokens) and authorization (roles, no IDOR)
- layered rate limiting: per IP, per user, and brute-force protection on login
- TLS with hot-reloaded certificates and an encrypted database connection
How this series works
Every post is self-contained. It opens with where we are (the state of the project at the end of the previous post), explains one idea, shows the code, and closes with a section called Complete code for this part: every file you have to create, in full, and every file you have to change, as a replacement or a diff.
Start from an empty folder in Part 1 and apply each post in order. After every part the project compiles, passes go vet, and runs. I checked that mechanically: a script reads the published posts, applies those blocks to an empty directory, and compares the result byte for byte with the real project at each step, then builds it and runs its tests.
Two things are deliberately not typed by hand: go.mod and go.sum (run go mod tidy when a post says so), and most test files. Tests appear as excerpts that explain what they prove, and you can write them yourself as an exercise. The one exception is the contract test in Part 16, which is given in full.
What you need
- Go 1.24 or newer (we use the method-and-path routing patterns from Go 1.22,
Request.Patternfrom 1.23, andt.Context()in tests from 1.24) - Docker with the compose plugin, from post 6 onwards
curl, and a terminal- Basic Go knowledge: structs, interfaces, goroutines, errors. You do not need to know any web framework.
The series map
| # | Post | You will learn |
|---|---|---|
| 1 | CRUD in memory | net/http routing, handlers, JSON, a mutex-protected store |
| 2 | Persistence with SQLite | database/sql, dependency injection, removing global state |
| 3 | Production basics | validation, sentinel errors, context, middleware, timeouts, graceful shutdown, tests |
| 4 | Configuration | twelve-factor config, fail-fast validation |
| 5 | A second resource | project structure, shared helpers, relations and cascades |
| 6 | Moving to PostgreSQL | pgx, pools, migrations, constraints, testing against a real database |
| 7 | Observability | logs, metrics, traces, health checks |
| 8 | Docker and docker compose | multi-stage images, healthchecks, Prometheus, Grafana, Jaeger, Loki |
| 9 | Pagination and API performance | keyset pagination, indexes, gzip, statement timeouts |
| 10 | Rate limiting | token buckets, client identification, spoofing defenses |
| 11 | Authentication | passwords, JWT, refresh-token rotation |
| 12 | Authorization | roles, ownership, the IDOR bug class |
| 13 | Protecting the login endpoint | layered limits, credential stuffing, timing attacks |
| 14 | TLS | certificates, hot reload, HSTS, encrypted DB connections |
| 15 | Idempotency keys | safe retries, exactly-once creates, atomicity with the database transaction |
| 16 | OpenAPI and the docs UI | a hand-written spec, Swagger UI under a strict CSP, contract tests that fail on drift |
| 17 | Wrap-up | the checklist, what we skipped, where to go next |
The finished architecture
The middleware chain runs, in order: tracing, request id, gzip, timeout, logging, metrics, panic recovery and rate limiting. The admin port is separate from the public one so that /metrics is never exposed to clients.
And the code layout we will arrive at, one layer at a time:
cmd/rest/ wiring: config → database → handlers → server
internal/config/ environment-based configuration
internal/routes/ URL → handler table
internal/handlers/ HTTP concerns: decode, validate, map errors to status codes
internal/storage/ SQL, and only SQL
internal/auth/ passwords and tokens (no HTTP, no SQL)
internal/middleware/ cross-cutting HTTP behavior
internal/ratelimit/ token-bucket limiter
internal/observability/ logs, metrics, traces, health
internal/tlsutil/ hot-reloading certificates
Dependencies only point downward: handlers knows about storage, never the reverse; auth and ratelimit know nothing about HTTP. This makes every layer testable on its own, and it is the single most valuable structural habit in the series.
What "production-grade" means here
We are not claiming a finished product, because that depends on your environment. We mean the app handles the situations that tutorials usually ignore:
| Situation | Where we handle it |
|---|---|
| Hostile or sloppy input | posts 3, 12 |
| A slow or dead database | posts 3, 6, 9 |
| Deploys and restarts | posts 3, 8 |
| "Why is it slow / failing?" | post 7 |
| Abusive clients | posts 10, 13 |
| Retries and double-clicks | post 15 |
| Docs that lie, clients that guess | post 16 |
| Stolen passwords, tokens, traffic | posts 11, 12, 14 |
At the end (post 17) we list honestly what is still missing, for example CI, Alertmanager and a real certificate authority.
A word on style
Every post follows the same rhythm: the problem, the fix with real code, try it with real output, why it works (the concept worth remembering), and exercises. The outputs you see were produced by running the code at that tag, not written from memory.
Ready? In the next post we write the smallest thing that could possibly work.
Comments
Join the discussionNo comments yet. Be the first to comment.
Build a production-grade REST API in Go
A hands-on series: start with under 200 lines of in-memory CRUD and end with Postgres, JWT auth, rate limiting, TLS and a full observability stack.