# Part 3: Production basics: validation, errors, context, middleware, graceful shutdown

*Ten problems from Part 1, fixed. This is the post that turns a demo into something you could put behind a load balancer.*

---

**Series:** Build a production-grade REST API in Go · **Part 3 of 17** **Where we are:** the SQLite-backed API from Part 2.

This is the longest early post, and the most valuable. Each section below removes a specific way the app can hurt you. Remember our table from Part 1? By the end, problems 1, 3, 4, 5, 6, 7, 8, 9 and 10 are gone.

## 1\. Errors you can test with `errors.Is`

In Part 2 the handler compared `err.Error() == "user not found"`. Strings are a terrible API: reword the message and a 404 becomes a 500, with no compiler warning.

The fix is a **sentinel error**: one package-level value that everyone compares against.

`internal/storage/storage.go (excerpt)`

```go
// ErrNotFound is a *sentinel error*: a package-level value callers compare
// against with errors.Is.
//
// Why not errors.New("user not found") inline and compare err.Error()?
// String comparison is brittle: reword the message, or wrap the error with
// fmt.Errorf("...: %w", err), and the comparison silently stops matching, so a
// 404 turns into a 500. errors.Is walks the whole wrap chain, so it keeps
// working however many layers add context.
var ErrNotFound = errors.New("user not found")
```

Callers use `errors.Is(err, storage.ErrNotFound)`. The crucial property: `errors.Is` walks the **wrap chain**. When a lower layer adds context with `fmt.Errorf("update user %d: %w", id, err)`, the `%w` verb keeps the original error inside, so the check still matches. A string comparison would have broken at the first wrap.

Rule of thumb: *return* errors with context (`%w`), *check* them with `errors.Is` / `errors.As`.

## 2\. Pass the request's `context` all the way down

Every `http.Request` carries a `context.Context`. It is cancelled when the client disconnects or a deadline passes. In Part 2 our store used `context.Background()`, which never cancels, so abandoned requests kept occupying the database.

Now every store method takes `ctx` as its first parameter and hands it to `QueryContext` / `ExecContext`:

`internal/storage/storage.go (excerpt)`

```go
// Update renames a user and returns the row as stored.
//
// `UPDATE ... RETURNING` (SQLite 3.35+) does the write and reads back the
// resulting row in one atomic statement. That fixes two things at once:
//   - the response reflects what is actually in the database, not an echo of
//     the request body;
//   - "no row matched" shows up as sql.ErrNoRows, so no separate
//     RowsAffected() call is needed.
func (s *UserStore) Update(ctx context.Context, id int, name string) (*User, error) {
	u := &User{}
	err := s.db.QueryRowContext(ctx,
		"UPDATE users SET name = ? WHERE id = ? RETURNING id, name",
		name, id,
	).Scan(&u.ID, &u.Name)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("update user %d: %w", id, err)
	}
	return u, nil
}

// Delete returns only an error. The old (bool, error) signature was redundant:
// the bool could only ever be true when err == nil. Make illegal states
// unrepresentable: success is nil, absence is ErrNotFound.
func (s *UserStore) Delete(ctx context.Context, id int) error {
	res, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", id)
	if err != nil {
		return fmt.Errorf("delete user %d: %w", id, err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("rows affected: %w", err)
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}
```

Two more improvements are visible here:

- `UPDATE ... RETURNING`: one statement writes *and* reads back the stored row, so the API returns what is actually in the database instead of echoing the request. Zero rows means not found (`sql.ErrNoRows`), so no separate `RowsAffected` call is needed. (SQLite supports `RETURNING` since 3.35; PostgreSQL has had it for years.)
- `Delete` **returns only** `error`. The old `(bool, error)` signature could only ever return `true` when `err == nil`: a value that carries no information. *Make illegal states unrepresentable.*

## 3\. Never trust the request body

Our Part 1 API accepted `{}` and created a nameless user. Input validation lives in one function:

`internal/handlers/handlers.go (excerpt)`

```go
// userInput is the request-body shape. Using a separate type (instead of
// decoding straight into storage.User) means a client-supplied "id" can never
// leak through: the server owns IDs.
type userInput struct {
	Name string `json:"name"`
}

// decodeUser reads, size-limits, strictly decodes and validates the body.
func (h *Handler) decodeUser(w http.ResponseWriter, r *http.Request) (userInput, bool) {
	// MaxBytesReader (unlike io.LimitReader) makes reads fail with an error
	// once the limit is hit and tells the server to close the connection.
	r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)

	dec := json.NewDecoder(r.Body)
	// Reject unknown fields so typos ("nmae") fail loudly instead of being
	// silently ignored and producing an empty name.
	dec.DisallowUnknownFields()

	var in userInput
	if err := dec.Decode(&in); err != nil {
		var tooBig *http.MaxBytesError
		if errors.As(err, &tooBig) {
			h.writeError(w, r, http.StatusRequestEntityTooLarge, "request body too large")
			return in, false
		}
		h.writeError(w, r, http.StatusBadRequest, "invalid JSON body")
		return in, false
	}
	// A second Decode must hit EOF; otherwise the body had trailing garbage
	// such as `{"name":"a"}{"name":"b"}`.
	if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		h.writeError(w, r, http.StatusBadRequest, "body must contain a single JSON object")
		return in, false
	}

	in.Name = strings.TrimSpace(in.Name)
	switch {
	case in.Name == "":
		h.writeError(w, r, http.StatusUnprocessableEntity, "name is required")
		return in, false
	case utf8.RuneCountInString(in.Name) > maxNameLen:
		h.writeError(w, r, http.StatusUnprocessableEntity, "name must be at most 100 characters")
		return in, false
	}
	return in, true
}
```

Line by line, here is what each defense stops:

| Defense | Without it |
| --- | --- |
| `http.MaxBytesReader` (1 MiB) | a 5 GB body is read into memory: free denial of service. Unlike `io.LimitReader`, it makes reads *fail* and tells the server to close the connection |
| `DisallowUnknownFields()` | a typo like `{"nmae":"Ann"}` silently creates a nameless user |
| second `Decode` must return `io.EOF` | trailing junk like `{"name":"a"}{"name":"b"}` is accepted: ambiguous input that other components in front of you might read differently |
| `strings.TrimSpace` + empty check | `" "` passes as a name |
| `utf8.RuneCountInString` ≤ 100 | counts *characters*, not bytes, so non-English names aren't unfairly rejected |

Also notice `userInput`: a **separate type for the request body** instead of decoding straight into `storage.User`. If we decoded into `User`, a client could send `{"id": 1, "name": "x"}` and set an id. Request shapes and database shapes are different contracts; keep them different types. We will rely on this heavily in the authorization post.

**Which status code?** `400` = "I could not parse what you sent", `413` = body too large, `422 Unprocessable Entity` = "I understood it, but it is not acceptable" (empty name). The distinction helps clients decide whether to fix syntax or data.

## 4\. One place for error responses, and don't leak internals

Plain `http.Error` text bodies were inconsistent with our JSON successes. We now centralize responses:

`internal/handlers/handlers.go (excerpt)`

```go
// storeError maps domain errors to HTTP status codes in ONE place, replacing
// the copy-pasted err.Error() == "user not found" checks.
func (h *Handler) storeError(w http.ResponseWriter, r *http.Request, op string, err error) {
	if errors.Is(err, storage.ErrNotFound) {
		h.writeError(w, r, http.StatusNotFound, "user not found")
		return
	}
	h.internalError(w, r, op, err)
}

// internalError logs the real cause server-side but tells the client nothing
// about internals (paths, SQL, driver messages can leak useful info to an
// attacker). The client gets a generic message; operators get the detail.
func (h *Handler) internalError(w http.ResponseWriter, r *http.Request, op string, err error) {
	// A cancelled request context is the client leaving, not a server fault.
	if errors.Is(err, context.Canceled) {
		h.log.Info("request cancelled", "op", op, "method", r.Method, "path", r.URL.Path)
		return
	}
	h.log.Error("handler failed", "op", op, "method", r.Method, "path", r.URL.Path, "err", err)
	h.writeError(w, r, http.StatusInternalServerError, "internal server error")
}

func (h *Handler) writeError(w http.ResponseWriter, r *http.Request, status int, msg string) {
	h.writeJSON(w, r, status, map[string]string{"error": msg})
}

// writeJSON centralises response writing. Errors are not ignored: by the time
// Encode fails the status line is usually already sent so we cannot change the
// response, but we can (and must) log it.
func (h *Handler) writeJSON(w http.ResponseWriter, r *http.Request, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(v); err != nil {
		h.log.Error("encode response", "path", r.URL.Path, "err", err)
	}
}
```

Design points:

- **Clients get a stable shape** (`{"error": "..."}`), so they can handle failures without parsing prose.
- **Internal errors are logged, not returned.** A raw database error can reveal table names, file paths, driver versions: gifts to an attacker. The client sees "internal server error"; the operator gets the full cause in the log.
- `context.Canceled` **is not a server fault.** When a client hangs up mid-request, we log it at INFO and don't count it as an error.
- `writeJSON` **checks** `Encode`**'s error.** By the time it fails the status line is usually sent, so we can't change the response, but we can and must log it (problem #10).
- Status mapping (`ErrNotFound` → 404) lives in `storeError`, once, instead of being copy-pasted into three handlers.

## 5\. Handlers depend on an interface, not a database

Look at the top of `handlers.go`:

```go
type UserStore interface {
	GetAll(ctx context.Context) ([]*storage.User, error)
	Create(ctx context.Context, name string) (*storage.User, error)
	Update(ctx context.Context, id int, name string) (*storage.User, error)
	Delete(ctx context.Context, id int) error
}
```

This is the Go proverb **"accept interfaces, return structs"** in practice. The interface is declared *where it is used* (the handlers), not where it is implemented (storage). `*storage.UserStore` satisfies it automatically, because Go interfaces are implicit. The payoff arrives in Part 6, when we replace SQLite with PostgreSQL and **the handlers do not change at all**.

## 6\. Middleware: behavior around every request

Logging and panic recovery apply to *every* route, so they don't belong in handlers. A middleware is just a function that wraps a handler:

```go
func(next http.Handler) http.Handler
```

`internal/middleware/middleware.go (excerpt)`

```go
// Recover turns a panic in any handler into a 500 instead of letting it
// unwind. (net/http already recovers per-connection, but it just closes the
// connection with no useful log; we log the panic with context.)
func Recover(log *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			defer func() {
				if rec := recover(); rec != nil {
					log.Error("panic recovered", "panic", rec, "method", r.Method, "path", r.URL.Path)
					http.Error(w, "internal server error", http.StatusInternalServerError)
				}
			}()
			next.ServeHTTP(w, r)
		})
	}
}

// statusRecorder remembers the status code, which http.ResponseWriter does not
// let us read back after the fact.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

// Logging emits one structured line per request: method, path, status, latency.
func Logging(log *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
			next.ServeHTTP(rec, r)
			log.Info("request",
				"method", r.Method,
				"path", r.URL.Path,
				"status", rec.status,
				"duration", time.Since(start),
			)
		})
	}
}

// Chain applies middlewares so the first listed is the outermost.
func Chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}
```

- `Recover`: if a handler panics, `net/http` prints a stack trace to stderr and drops the connection: no response for the client and no structured log. We turn it into a logged 500 with request context.
- `statusRecorder`: `http.ResponseWriter` has no way to read back the status code that was written, so we wrap it and remember it. (A pattern you will see in every Go logging middleware.)
- `Logging`: one structured line per request with method, path, status and duration.
- `Chain`: applies middlewares so the first one listed is the outermost.

We use Go's `log/slog` for **structured logging**: JSON objects, one per line, that tools like Loki or Elasticsearch can query by field. Compare `GET /users 200 1.2ms` (text you must regex) with `{"msg":"request","status":200,"duration":1203}` (data you can filter and graph).

## 7\. A server that survives the real world

The zero-value `http.Server` has **no timeouts at all**. A malicious (or just slow, mobile) client can open a connection and dribble bytes forever ("Slowloris"), tying up a goroutine and file descriptor each time. And `Ctrl-C` used to kill in-flight requests mid-write.

Both are fixed in `main`. Notice we moved the body into `run() error`:

`cmd/rest/main.go (excerpt)`

```go
func run() error {
	log := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	addr := getenv("ADDR", ":7800")
	// An absolute path (or one set via env) avoids silently creating a
	// different users.db depending on the working directory.
	dbPath := getenv("DB_PATH", "users.db")

	// ctx is cancelled on Ctrl-C (SIGINT) or SIGTERM (what Docker/Kubernetes
	// send on stop), which triggers graceful shutdown below.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	db, err := storage.InitDB(ctx, dbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	h := handlers.NewHandler(storage.NewUserStore(db), log)
	mux := http.NewServeMux()
	routes.SetupRoutes(mux, h)

	srv := &http.Server{
		Addr:    addr,
		Handler: middleware.Chain(mux, middleware.Logging(log), middleware.Recover(log)),
		// The zero-value http.Server has NO timeouts, so slow or malicious
		// clients (e.g. Slowloris) can hold connections open forever.
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      15 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	// ListenAndServe blocks, so run it in a goroutine and wait on either a
	// server failure or a shutdown signal.
	serverErr := make(chan error, 1)
	go func() {
		log.Info("server starting", "addr", addr, "db", dbPath)
		serverErr <- srv.ListenAndServe()
	}()

	select {
	case err := <-serverErr:
		// ErrServerClosed is the normal result of Shutdown, not a failure.
		if !errors.Is(err, http.ErrServerClosed) {
			return err
		}
	case <-ctx.Done():
		log.Info("shutdown signal received")
	}

	// Stop accepting new connections and wait (bounded) for in-flight requests
	// to finish, so a half-done write is not killed mid-request. Only then does
	// the deferred db.Close() run.
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		return err
	}
	log.Info("server stopped cleanly")
	return nil
}
```

Study it in three parts:

**Timeouts.** `ReadHeaderTimeout` (time to send headers), `ReadTimeout` (whole request), `WriteTimeout` (whole response), `IdleTimeout` (keep-alive connections waiting for the next request). Set all four.

**Signals.** `signal.NotifyContext` gives us a context that is cancelled on `SIGINT` (Ctrl-C) or `SIGTERM`. `SIGTERM` is what Docker and Kubernetes send when they stop a container, so handling it is mandatory in production.

**Graceful shutdown.** On the signal we call `srv.Shutdown(ctx)`: stop accepting new connections, let in-flight requests finish (up to 15 s), then return. Only after that does the deferred `db.Close()` run, so no request ever loses its database mid-flight.

**Why** `run()`**?** `log.Fatal` calls `os.Exit`, which **skips deferred functions**: `db.Close()` would never run. By returning an error from `run()` and only exiting in `main`, every `defer` executes first. A small pattern that prevents a class of bugs.

## 8\. SQLite settings that matter

`InitDB` now passes settings in the connection string:

- `journal_mode(WAL)`: write-ahead logging lets readers and one writer work simultaneously.
- `busy_timeout(5000)`: wait up to 5 s for a lock instead of failing instantly.
- `foreign_keys(1)`: SQLite ignores foreign keys unless asked. We don't use them yet; we will in Part 5.

They go in the DSN (not in a one-off `db.Exec("PRAGMA ...")`) because PRAGMAs are *per connection* and `database/sql` may open new connections at any time. The DSN applies to every connection in the pool.

## 9\. Tests that exercise the real stack

`internal/handlers/handlers_test.go (excerpt)`

```go
// newServer wires the real router, handlers and a temp SQLite store, so these
// tests exercise the full HTTP stack end to end.
func newServer(t *testing.T) *httptest.Server {
	t.Helper()
	db, err := storage.InitDB(t.Context(), filepath.Join(t.TempDir(), "t.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })

	h := handlers.NewHandler(storage.NewUserStore(db), slog.New(slog.NewTextHandler(io.Discard, nil)))
	mux := http.NewServeMux()
	routes.SetupRoutes(mux, h)
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv
}

func do(t *testing.T, method, url, body string) (int, string) {
	t.Helper()
	req, _ := http.NewRequest(method, url, strings.NewReader(body))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(b)
}
```

`httptest.NewServer` starts a real HTTP server on a random port, wired with the **real router, real handlers and a real (temporary) SQLite database**. Then a table of cases:

`internal/handlers/handlers_test.go (lines 51-69)`

```go
	tests := []struct {
		name, method, path, body string
		want                     int
	}{
		{"create ok", "POST", u, `{"name":"ann"}`, 201},
		{"create empty name", "POST", u, `{}`, 422},
		{"create blank name", "POST", u, `{"name":"  "}`, 422},
		{"create unknown field", "POST", u, `{"name":"a","id":9}`, 400},
		{"create bad json", "POST", u, `{`, 400},
		{"create trailing data", "POST", u, `{"name":"a"}{"name":"b"}`, 400},
		{"create too long", "POST", u, `{"name":"` + strings.Repeat("a", 101) + `"}`, 422},
		{"create too big", "POST", u, `{"name":"` + strings.Repeat("a", 2<<20) + `"}`, 413},
		{"list", "GET", u, "", 200},
		{"update ok", "PUT", u + "/1", `{"name":"anna"}`, 200},
		{"update missing", "PUT", u + "/999", `{"name":"x"}`, 404},
		{"update bad id", "PUT", u + "/abc", `{"name":"x"}`, 400},
		{"delete ok", "DELETE", u + "/1", "", 204},
		{"delete again", "DELETE", u + "/1", "", 404},
	}
```

Why test this way rather than mocking the store? Because most bugs live in the *seams*: routing patterns, status codes, JSON tags, SQL. A test that goes through all of them catches what unit tests with mocks cannot. Always run with the race detector:

```bash
go test -race ./...
```

## Complete code for this part

Everything you need to type, file by file. Paste them in any order, then run `go mod tidy` and `go build ./...`. (The test files are shown above as the interesting parts only; the code below is complete without them.)

**Replace the contents of** `internal/handlers/handlers.go` **with**

```go
package handlers

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"unicode/utf8"

	"go-rest/internal/storage"
)

const (
	// maxBodyBytes caps request bodies. Without a cap, a client can stream
	// gigabytes at json.Decoder and exhaust memory.
	maxBodyBytes = 1 << 20 // 1 MiB

	maxNameLen = 100
)

// UserStore is the behaviour the handlers need, declared *here, where it is
// used* ("accept interfaces, return structs"). Benefits: handlers can be
// unit-tested with a fake store, and they no longer depend on the concrete
// SQLite-backed type. *storage.UserStore satisfies it implicitly.
type UserStore interface {
	GetAll(ctx context.Context) ([]*storage.User, error)
	Create(ctx context.Context, name string) (*storage.User, error)
	Update(ctx context.Context, id int, name string) (*storage.User, error)
	Delete(ctx context.Context, id int) error
}

type Handler struct {
	store UserStore
	log   *slog.Logger
}

func NewHandler(store UserStore, log *slog.Logger) *Handler {
	return &Handler{store: store, log: log}
}

// userInput is the request-body shape. Using a separate type (instead of
// decoding straight into storage.User) means a client-supplied "id" can never
// leak through: the server owns IDs.
type userInput struct {
	Name string `json:"name"`
}

func (h *Handler) HandleGetUsers(w http.ResponseWriter, r *http.Request) {
	users, err := h.store.GetAll(r.Context())
	if err != nil {
		h.internalError(w, r, "list users", err)
		return
	}
	h.writeJSON(w, r, http.StatusOK, users)
}

func (h *Handler) HandleCreateUsers(w http.ResponseWriter, r *http.Request) {
	in, ok := h.decodeUser(w, r)
	if !ok {
		return
	}
	created, err := h.store.Create(r.Context(), in.Name)
	if err != nil {
		h.internalError(w, r, "create user", err)
		return
	}
	h.writeJSON(w, r, http.StatusCreated, created)
}

func (h *Handler) HandleUpdateUsers(w http.ResponseWriter, r *http.Request) {
	id, ok := h.parseID(w, r)
	if !ok {
		return
	}
	in, ok := h.decodeUser(w, r)
	if !ok {
		return
	}
	updated, err := h.store.Update(r.Context(), id, in.Name)
	if err != nil {
		h.storeError(w, r, "update user", err)
		return
	}
	h.writeJSON(w, r, http.StatusOK, updated)
}

func (h *Handler) HandleDeleteUsers(w http.ResponseWriter, r *http.Request) {
	id, ok := h.parseID(w, r)
	if !ok {
		return
	}
	if err := h.store.Delete(r.Context(), id); err != nil {
		h.storeError(w, r, "delete user", err)
		return
	}
	// 204 No Content is the REST convention for a successful delete: nothing
	// useful to return, so send no body (and no Content-Type).
	w.WriteHeader(http.StatusNoContent)
}

// parseID extracts and validates the {id} path value. Returns ok=false after
// having already written the error response.
func (h *Handler) parseID(w http.ResponseWriter, r *http.Request) (int, bool) {
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil || id <= 0 {
		h.writeError(w, r, http.StatusBadRequest, "invalid user id")
		return 0, false
	}
	return id, true
}

// decodeUser reads, size-limits, strictly decodes and validates the body.
func (h *Handler) decodeUser(w http.ResponseWriter, r *http.Request) (userInput, bool) {
	// MaxBytesReader (unlike io.LimitReader) makes reads fail with an error
	// once the limit is hit and tells the server to close the connection.
	r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)

	dec := json.NewDecoder(r.Body)
	// Reject unknown fields so typos ("nmae") fail loudly instead of being
	// silently ignored and producing an empty name.
	dec.DisallowUnknownFields()

	var in userInput
	if err := dec.Decode(&in); err != nil {
		var tooBig *http.MaxBytesError
		if errors.As(err, &tooBig) {
			h.writeError(w, r, http.StatusRequestEntityTooLarge, "request body too large")
			return in, false
		}
		h.writeError(w, r, http.StatusBadRequest, "invalid JSON body")
		return in, false
	}
	// A second Decode must hit EOF; otherwise the body had trailing garbage
	// such as `{"name":"a"}{"name":"b"}`.
	if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		h.writeError(w, r, http.StatusBadRequest, "body must contain a single JSON object")
		return in, false
	}

	in.Name = strings.TrimSpace(in.Name)
	switch {
	case in.Name == "":
		h.writeError(w, r, http.StatusUnprocessableEntity, "name is required")
		return in, false
	case utf8.RuneCountInString(in.Name) > maxNameLen:
		h.writeError(w, r, http.StatusUnprocessableEntity, "name must be at most 100 characters")
		return in, false
	}
	return in, true
}

// storeError maps domain errors to HTTP status codes in ONE place, replacing
// the copy-pasted err.Error() == "user not found" checks.
func (h *Handler) storeError(w http.ResponseWriter, r *http.Request, op string, err error) {
	if errors.Is(err, storage.ErrNotFound) {
		h.writeError(w, r, http.StatusNotFound, "user not found")
		return
	}
	h.internalError(w, r, op, err)
}

// internalError logs the real cause server-side but tells the client nothing
// about internals (paths, SQL, driver messages can leak useful info to an
// attacker). The client gets a generic message; operators get the detail.
func (h *Handler) internalError(w http.ResponseWriter, r *http.Request, op string, err error) {
	// A cancelled request context is the client leaving, not a server fault.
	if errors.Is(err, context.Canceled) {
		h.log.Info("request cancelled", "op", op, "method", r.Method, "path", r.URL.Path)
		return
	}
	h.log.Error("handler failed", "op", op, "method", r.Method, "path", r.URL.Path, "err", err)
	h.writeError(w, r, http.StatusInternalServerError, "internal server error")
}

func (h *Handler) writeError(w http.ResponseWriter, r *http.Request, status int, msg string) {
	h.writeJSON(w, r, status, map[string]string{"error": msg})
}

// writeJSON centralises response writing. Errors are not ignored: by the time
// Encode fails the status line is usually already sent so we cannot change the
// response, but we can (and must) log it.
func (h *Handler) writeJSON(w http.ResponseWriter, r *http.Request, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(v); err != nil {
		h.log.Error("encode response", "path", r.URL.Path, "err", err)
	}
}
```

**Create** `internal/middleware/middleware.go`

```go
// Package middleware holds cross-cutting HTTP concerns. A middleware is just
// func(http.Handler) http.Handler: it wraps the next handler, running code
// before and/or after it.
package middleware

import (
	"log/slog"
	"net/http"
	"time"
)

// Recover turns a panic in any handler into a 500 instead of letting it
// unwind. (net/http already recovers per-connection, but it just closes the
// connection with no useful log; we log the panic with context.)
func Recover(log *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			defer func() {
				if rec := recover(); rec != nil {
					log.Error("panic recovered", "panic", rec, "method", r.Method, "path", r.URL.Path)
					http.Error(w, "internal server error", http.StatusInternalServerError)
				}
			}()
			next.ServeHTTP(w, r)
		})
	}
}

// statusRecorder remembers the status code, which http.ResponseWriter does not
// let us read back after the fact.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (s *statusRecorder) WriteHeader(code int) {
	s.status = code
	s.ResponseWriter.WriteHeader(code)
}

// Logging emits one structured line per request: method, path, status, latency.
func Logging(log *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
			next.ServeHTTP(rec, r)
			log.Info("request",
				"method", r.Method,
				"path", r.URL.Path,
				"status", rec.status,
				"duration", time.Since(start),
			)
		})
	}
}

// Chain applies middlewares so the first listed is the outermost.
func Chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}
```

**Replace the contents of** `internal/storage/storage.go` **with**

```go
package storage

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/url"

	_ "modernc.org/sqlite" // registers the "sqlite" driver with database/sql
)

// ErrNotFound is a *sentinel error*: a package-level value callers compare
// against with errors.Is.
//
// Why not errors.New("user not found") inline and compare err.Error()?
// String comparison is brittle: reword the message, or wrap the error with
// fmt.Errorf("...: %w", err), and the comparison silently stops matching, so a
// 404 turns into a 500. errors.Is walks the whole wrap chain, so it keeps
// working however many layers add context.
var ErrNotFound = errors.New("user not found")

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type UserStore struct {
	db *sql.DB
}

func NewUserStore(db *sql.DB) *UserStore {
	return &UserStore{db: db}
}

// InitDB opens (creating if needed) the SQLite database at path and applies
// the schema.
//
// Per-connection settings are passed as _pragma query parameters in the DSN.
// This matters: PRAGMAs are per *connection*, and database/sql may open new
// connections at any time. Running `db.Exec("PRAGMA ...")` once would only
// configure whichever connection happened to execute it. The DSN is applied to
// every connection the pool opens.
//
//   - busy_timeout(5000): if another connection/process holds the write lock,
//     wait up to 5s instead of failing instantly with SQLITE_BUSY.
//   - journal_mode(WAL): write-ahead logging lets readers and one writer work
//     concurrently (default rollback-journal mode blocks readers during
//     writes). It is also what creates the users.db-wal / -shm files that
//     .gitignore already lists. The mode is persisted in the file itself.
//   - synchronous(NORMAL): the recommended pairing with WAL; durable across
//     application crashes, trading only a tiny window on OS crash/power loss
//     for much faster commits than FULL.
//   - foreign_keys(1): SQLite ignores FOREIGN KEY constraints unless this is
//     on. Unused today, but enabling it now avoids a nasty surprise later.
func InitDB(ctx context.Context, path string) (*sql.DB, error) {
	q := url.Values{}
	q.Add("_pragma", "busy_timeout(5000)")
	q.Add("_pragma", "journal_mode(WAL)")
	q.Add("_pragma", "synchronous(NORMAL)")
	q.Add("_pragma", "foreign_keys(1)")
	dsn := "file:" + path + "?" + q.Encode()

	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("open database: %w", err)
	}

	// With WAL, many readers can run alongside one writer, so we no longer need
	// the old SetMaxOpenConns(1), which serialised *everything* behind a single
	// connection. A small bounded pool keeps concurrency without letting a
	// traffic spike open unlimited file handles. SQLite itself still allows only
	// one writer at a time; busy_timeout makes extra writers queue politely.
	db.SetMaxOpenConns(4)
	db.SetMaxIdleConns(4)

	const schema = `
		CREATE TABLE IF NOT EXISTS users (
			id   INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL CHECK (length(trim(name)) > 0)
		);`
	// The CHECK is defence in depth: handlers validate input, but the database
	// is the last line of defence against empty names from any other code path.

	// PingContext forces a real connection now, so a bad path or unwritable
	// directory fails at startup rather than on the first request.
	if err := db.PingContext(ctx); err != nil {
		db.Close()
		return nil, fmt.Errorf("ping database: %w", err)
	}
	if _, err := db.ExecContext(ctx, schema); err != nil {
		db.Close()
		return nil, fmt.Errorf("create schema: %w", err)
	}
	return db, nil
}

// Every method takes a context.Context as its first parameter and passes it to
// the *Context database calls. When the HTTP client disconnects or the request
// times out, the context is cancelled and the query is abandoned, so the
// connection goes back to the pool instead of being held by a dead request.
// (Previously these used context.Background(), which can never be cancelled.)

func (s *UserStore) GetAll(ctx context.Context) ([]*User, error) {
	rows, err := s.db.QueryContext(ctx, "SELECT id, name FROM users ORDER BY id")
	if err != nil {
		return nil, fmt.Errorf("query users: %w", err)
	}
	defer rows.Close()

	users := make([]*User, 0) // non-nil so JSON encodes [] rather than null
	for rows.Next() {
		u := &User{}
		if err := rows.Scan(&u.ID, &u.Name); err != nil {
			return nil, fmt.Errorf("scan user: %w", err)
		}
		users = append(users, u)
	}
	// rows.Err reports any error that ended iteration early (e.g. cancelled
	// context). Skipping this check can silently return a truncated list.
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate users: %w", err)
	}
	return users, nil
}

func (s *UserStore) Create(ctx context.Context, name string) (*User, error) {
	res, err := s.db.ExecContext(ctx, "INSERT INTO users (name) VALUES (?)", name)
	if err != nil {
		return nil, fmt.Errorf("insert user: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("last insert id: %w", err)
	}
	return &User{ID: int(id), Name: name}, nil
}

// Update renames a user and returns the row as stored.
//
// `UPDATE ... RETURNING` (SQLite 3.35+) does the write and reads back the
// resulting row in one atomic statement. That fixes two things at once:
//   - the response reflects what is actually in the database, not an echo of
//     the request body;
//   - "no row matched" shows up as sql.ErrNoRows, so no separate
//     RowsAffected() call is needed.
func (s *UserStore) Update(ctx context.Context, id int, name string) (*User, error) {
	u := &User{}
	err := s.db.QueryRowContext(ctx,
		"UPDATE users SET name = ? WHERE id = ? RETURNING id, name",
		name, id,
	).Scan(&u.ID, &u.Name)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("update user %d: %w", id, err)
	}
	return u, nil
}

// Delete returns only an error. The old (bool, error) signature was redundant:
// the bool could only ever be true when err == nil. Make illegal states
// unrepresentable: success is nil, absence is ErrNotFound.
func (s *UserStore) Delete(ctx context.Context, id int) error {
	res, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", id)
	if err != nil {
		return fmt.Errorf("delete user %d: %w", id, err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("rows affected: %w", err)
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}
```

**Replace the contents of** `cmd/rest/main.go` **with**

```go
package main

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"go-rest/internal/handlers"
	"go-rest/internal/middleware"
	"go-rest/internal/routes"
	"go-rest/internal/storage"
)

// getenv reads configuration from the environment (twelve-factor style) so
// the same binary runs anywhere without recompiling.
func getenv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func main() {
	// run() returns an error instead of calling log.Fatal inside main's body.
	// log.Fatal calls os.Exit, which skips deferred functions, so a deferred
	// db.Close() would never run. Returning lets every defer execute first.
	if err := run(); err != nil {
		slog.Error("fatal", "err", err)
		os.Exit(1)
	}
}

func run() error {
	log := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	addr := getenv("ADDR", ":7800")
	// An absolute path (or one set via env) avoids silently creating a
	// different users.db depending on the working directory.
	dbPath := getenv("DB_PATH", "users.db")

	// ctx is cancelled on Ctrl-C (SIGINT) or SIGTERM (what Docker/Kubernetes
	// send on stop), which triggers graceful shutdown below.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	db, err := storage.InitDB(ctx, dbPath)
	if err != nil {
		return err
	}
	defer db.Close()

	h := handlers.NewHandler(storage.NewUserStore(db), log)
	mux := http.NewServeMux()
	routes.SetupRoutes(mux, h)

	srv := &http.Server{
		Addr:    addr,
		Handler: middleware.Chain(mux, middleware.Logging(log), middleware.Recover(log)),
		// The zero-value http.Server has NO timeouts, so slow or malicious
		// clients (e.g. Slowloris) can hold connections open forever.
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      15 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	// ListenAndServe blocks, so run it in a goroutine and wait on either a
	// server failure or a shutdown signal.
	serverErr := make(chan error, 1)
	go func() {
		log.Info("server starting", "addr", addr, "db", dbPath)
		serverErr <- srv.ListenAndServe()
	}()

	select {
	case err := <-serverErr:
		// ErrServerClosed is the normal result of Shutdown, not a failure.
		if !errors.Is(err, http.ErrServerClosed) {
			return err
		}
	case <-ctx.Done():
		log.Info("shutdown signal received")
	}

	// Stop accepting new connections and wait (bounded) for in-flight requests
	// to finish, so a half-done write is not killed mid-request. Only then does
	// the deferred db.Close() run.
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		return err
	}
	log.Info("server stopped cleanly")
	return nil
}
```

**Replace the contents of** `makefile` **with**

```makefile
.PHONY: run build test clean clean-db db-shell

DB_FILE ?= users.db
BINARY := bin/server

# Export so the server and the targets below agree on the same database file.
export DB_PATH := $(abspath $(DB_FILE))

run:
	go run ./cmd/rest

build:
	mkdir -p bin
	go build -o $(BINARY) ./cmd/rest

test:
	go vet ./...
	go test -race ./...

# Removes build artifacts only; data is deliberately kept.
clean:
	rm -f $(BINARY)

# Destructive: deletes the database and its WAL/SHM sidecar files.
clean-db:
	rm -f $(DB_FILE) $(DB_FILE)-wal $(DB_FILE)-shm

db-shell:
	@command -v sqlite3 >/dev/null || { echo "sqlite3 CLI not installed"; exit 1; }
	sqlite3 $(DB_PATH)
```

## Try it

```bash
$ curl -i -XPOST localhost:7800/users -d '{}'
HTTP/1.1 422 Unprocessable Entity
{"error":"name is required"}

$ curl -i -XPOST localhost:7800/users -d '{"name":"a","id":9}'
HTTP/1.1 400 Bad Request
{"error":"invalid JSON body"}

$ curl -i -XPOST localhost:7800/users -d '{"name":"   "}'
HTTP/1.1 422 Unprocessable Entity
{"error":"name is required"}

$ curl -i -XPOST localhost:7800/users --data-binary @2MB.json
HTTP/1.1 413 Request Entity Too Large
{"error":"request body too large"}

$ curl -i -XPUT localhost:7800/users/99 -d '{"name":"x"}'
HTTP/1.1 404 Not Found
{"error":"user not found"}
```

And in the server terminal, one JSON log line per request, then on `Ctrl-C`:

```json
{"time":"...","level":"INFO","msg":"request","method":"POST","path":"/users","status":422,"duration":541291}
{"time":"...","level":"INFO","msg":"shutdown signal received"}
{"time":"...","level":"INFO","msg":"server stopped cleanly"}
```

## Scorecard

| # | Problem from Part 1 | Status |
| --- | --- | --- |
| 1 | No validation | ✅ strict decode + checks |
| 2 | In-memory | ✅ (Part 2) |
| 3 | Global variable | ✅ (Part 2) |
| 4 | Random list order | ✅ `ORDER BY id` |
| 5 | Plain-text errors | ✅ JSON error shape |
| 6 | Unbounded body | ✅ 1 MiB cap |
| 7 | No timeouts | ✅ four timeouts |
| 8 | No graceful shutdown | ✅ SIGTERM + `Shutdown` |
| 9 | Shared pointers | ✅ values come from the DB |
| 10 | Ignored `Encode` errors | ✅ logged |

## Recap

- Sentinel errors + `errors.Is` + `%w` instead of string matching.
- Thread `context` from the request down to the database.
- Validate and bound every input; separate request types from storage types.
- Log the cause, return a generic message.
- Depend on interfaces declared by the consumer.
- Middleware for cross-cutting concerns; timeouts and graceful shutdown on the server.

## Exercises

1. Add a middleware that rejects requests whose `Content-Type` is not `application/json` on POST/PUT with 415.
2. Write a test that proves `Recover` works: a handler that panics must yield a 500 and the server must keep serving.
3. Send `kill -TERM` to the server while a slow request is in flight (add a `time.Sleep(5*time.Second)` to a handler). Does the request finish? Now set `Shutdown`'s timeout to 1 s and repeat.
