Step 7 — Orders, and getting it ready to deploy
The last endpoints, then logging, limits, a container and the hardening pass.
Topic 81 of 98 · 5 min to read · 50 min to build
After this topic you can
- Build order endpoints that are scoped to the user, paged and joined.
- Add structured logging and rate limits to the shop.
- Containerise the service and run the hardening checks before shipping.
Orders: scoped, paged, and joined
Nothing new here. It is the same three rules from earlier projects, applied together.
internal/adapter/storage/postgres/order.go
func (r *Repository) ListByUser(ctx context.Context, userID int64, limit, offset int) ([]Order, error) { rows, err := r.db.QueryContext(ctx, ` SELECT o.id, o.total_cents, o.status, o.created_at, count(oi.id) AS line_count FROM orders o LEFT JOIN order_items oi ON oi.order_id = o.id WHERE o.user_id = $1 -- scoped, always GROUP BY o.id ORDER BY o.created_at DESC LIMIT $2 OFFSET $3 -- paged, always `, userID, limit, offset) // ... scan rows exactly as in step 5}Structured logging
cmd/api/main.go
var h slog.Handlerif cfg.Env == "production" { h = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})} else { h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})}slog.SetDefault(slog.New(h))slog.Info("order placed", "order_id", o.ID, "user_id", userID, "total_cents", o.TotalCents, "items", len(items))Rate limits where they matter
cmd/api/main.go
authLimit := middleware.RateLimit(0.2, 5) // ~12 a minute, burst 5apiLimit := middleware.RateLimit(20, 40) // generous for normal usemux.Handle("POST /v1/auth/login", authLimit(http.HandlerFunc(userH.Login)))mux.Handle("POST /v1/auth/register", authLimit(http.HandlerFunc(userH.Register)))mux.Handle("POST /v1/checkout", apiLimit(protect(http.HandlerFunc(orderH.Checkout))))Every command to containerise it
terminal
# 1. the Dockerfilecat > Dockerfile <<'EOF'FROM golang:1.22-alpine AS buildWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /api ./cmd/apiFROM gcr.io/distroless/static-debian12COPY --from=build /api /apiUSER nonroot:nonrootEXPOSE 8080ENTRYPOINT ["/api"]EOF# 2. build and check the sizedocker build -t shop:latest .docker images shop:latest# 3. run the whole stackdocker compose up -ddocker compose logs -f api# 4. useful while developingdocker compose down # stopdocker compose down -v # stop and wipe the databasedocker compose exec db psql -U postgres -d shopThe two container settings people miss
docker-compose.yml — the api service environment
environment: DATABASE_URL: postgres://postgres:secret@db:5432/shop?sslmode=disable JWT_SECRET: dev-secret-at-least-32-characters-long UPLOAD_DIR: /data/uploads GOMEMLIMIT: 450MiB # ~90% of the memory limit GOMAXPROCS: "2" # match the CPU limit, not the hostvolumes: - uploads:/data/uploads # uploads must outlive the containerThe hardening pass
- Every route’s auth — grep your
Routesfunctions for a handler registered withoutprotect, and every admin route withoutadminOnly. - Every query’s scope —
WHERE id = $1with noAND user_id = $2on anything user-owned. - Every query’s SQL — any
fmt.Sprintfor+near a query string that touches a request value. - Every file path — anything built from something the client sent.
- Every log line — a password, a token, or a whole body.
- Every goroutine — one with no way to exit.
The four checks, then ship
terminal
gofmt -l . # must print nothinggo vet ./... # must be silentgo test -race ./... # must be greengo run golang.org/x/vuln/cmd/govulncheck@latest ./...# then a Makefile so you stop retypingcat > Makefile <<'EOF'run: ; go run ./cmd/apitest: ; go test -race -cover ./...lint: ; go vet ./... && gofmt -l .build: ; CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/api ./cmd/apiup: ; docker compose up -ddown: ; docker compose downmigrate: ; migrate -path migrations -database "$(DATABASE_URL)" upcheck: ; gofmt -l . && go vet ./... && go test -race ./... EOFmake checkWorth knowing: Three projects done. You have built a public service, a service with accounts and private data, and a service with roles, files and money that cannot be lost. That is a real portfolio, and every idea in the interview topics now has something of yours to point at.
The short version
- Orders are the same three rules again: scoped, paged, joined rather than looped.
- Structured logs with
slog: JSON in production, and never log secrets. - Rate-limit login and register hardest. That is where attacks land.
GOMEMLIMITandGOMAXPROCSin every container, and a volume for uploads.
Remember: Shipping is pagination, logging, limits, a container and one hardening pass. Each is small; together they are the difference between a demo and a service.
Build this before moving on
Finish orders, add logging and rate limits, containerise it, and work the hardening checklist line by line. Then run make check and get it green.Then try these in your own editor
- 1Hammer login with a curl loop until you get 429, and check the
Retry-Afterheader. - 2Restart the container and confirm uploaded images are still there. Then remove the volume and see them vanish.
- 3Set
GOMEMLIMITfar too low and watch the garbage collector work harder in the logs. - 4Find one thing to delete: a helper used once, or an interface with one implementation.
Official documentation