Skip to main content

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.

Updated 6 min readMarkdown
Part of seriesBuild a production-grade REST API in Go
Build a production-grade REST API in Go, step by step

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/http service (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.Pattern from 1.23, and t.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

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.

No comments yet. Be the first to comment.

Build a production-grade REST API in Go

Part 1 of 18

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.

Up nextPart 1: CRUD in memory with nothing but net/httpCreate, read, update, delete: the smallest useful HTTP API in Go, with no framework and no database.

More from this blog