# Gin From Zero

> Learn Go's most popular web framework: the engine and your first server, routing and route groups, binding and validating JSON, responses and rendering, middleware, building a full REST API, error handling and project structure, and testing and production. A thin, fast layer over net/http that you'll meet in most Go web jobs.


---

# Gin From Zero

If you write a web service in Go, there's a very good chance it's a Gin service. Gin is the most popular
Go web framework: a thin, fast layer over the standard library's `net/http` that hands you the things you
write by hand otherwise - a real router with URL parameters, JSON binding and validation, middleware, and
tidy response helpers - without hiding what Go is actually doing underneath. That last part matters: Gin
is *small*. Once you've seen it, it reads as "net/http with the boring parts done for you," not magic.

The mental model is one object and one value. The **engine** (`gin.Engine`) is your application - you
register routes on it and run it. Every request that arrives gets a **context** (`*gin.Context`) - one
value that carries the request, the response writer, the parsed parameters, and the helpers to read input
and write output. Learn to think "engine holds the routes, context handles the request," and the whole
framework falls into place.

> 📝 This teaches the **framework** - it assumes you know **Go**: functions, structs, methods, interfaces,
> and `error` ([Go From Zero](/guides/go-from-zero)). It pairs with [What a Framework Even Is](/guides/what-a-framework-even-is),
> and it's worth comparing with [Echo](/guides/echo-from-zero) and [chi](/guides/chi-from-zero); the
> [net/http roots guide](/guides/web-services-with-only-net-http) shows what Gin is built on. Gin compiles
> and runs as a Go program, so examples are shown with the commands to run them yourself.

## How to read this

Read in order - it grows one service (a small **tasks API**) from a single route to a structured, tested,
deployable REST API. Phases carry difficulty badges.

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What Gin Is & Your First Server](01-what-gin-is.md)** 🟢 - the engine, the context, and a running server in a few lines.
2. **[Routing & Route Groups](02-routing-and-groups.md)** 🟢 - methods, path and query params, wildcards, and grouping routes.
3. **[Binding & Validating Input](03-binding-and-validation.md)** 🟡 - `ShouldBindJSON`, struct tags, and the built-in validator.

**Part 2 - A real API (🟡 → 🔴)**
4. **[Responses & Rendering](04-responses-and-rendering.md)** 🟡 - `c.JSON`, status codes, HTML templates, and static files.
5. **[Middleware](05-middleware.md)** 🟡 - what middleware is, `c.Next()`, the built-in Logger/Recovery, and writing your own.
6. **[Building a REST API](06-building-a-rest-api.md)** 🟡 - full CRUD for the tasks resource, wired end to end.
7. **[Error Handling & Project Structure](07-errors-and-structure.md)** 🔴 - `c.Error`, `AbortWithStatusJSON`, and structuring beyond one file.

**Part 3 - Ship it (🟡 → 🟢)**
8. **[Testing & Production](08-testing-and-production.md)** 🟡 - `httptest` with Gin, test mode, graceful shutdown, and deployment.
9. **[Where to Go Next](09-where-to-go-next.md)** 🟢 - Gin vs Echo/chi/Fiber, when plain net/http is enough, and what to build.

> The throughline: an **engine** holds your routes, a **context** handles each request, and middleware
> wraps the chain. Hold those three and Gin is a small, fast tool you fully understand.


---

# What Gin Is & Your First Server

You know [Go](/guides/go-from-zero), and you want to put something on the web. Here's the plain
truth: Go can already do that with nothing but its standard library. The `net/http` package gives you
a working HTTP server in a handful of lines - no framework required. We even have a whole guide on it:
[Web Services with Only net/http](/guides/web-services-with-only-net-http).

So why reach for Gin at all? Because once you go past "hello world," the stdlib makes you hand-roll the
boring, repetitive parts: your own routing logic to tell `/tasks/42` apart from `/tasks`, JSON marshalled
by hand with the `Content-Type` header set every time, your own middleware plumbing for logging and panic
recovery. None of it is *hard*, exactly - it's just the same wiring, written again, on every project.

💡 **Gin does the boring parts and stays out of the way.** It's the most popular Go web framework: a
*thin, fast* layer over `net/http` that hands you a real router, JSON helpers, and middleware - without
hiding the standard library underneath. Gin is small. Once you've seen it, it reads as "net/http with
the tedious bits done for you," not magic.

## The mental model: engine holds the routes, context handles the request

Before any code, hold these two things in your head. They are the whole framework.

📝 **The engine** (`*gin.Engine`) is your application. It's the object you create once, register all
your routes on, and then run. When people say "the Gin app," they mean the engine.

📝 **The context** (`*gin.Context`) is one value handed to you for *each incoming request*. It carries
the request, the response writer, the URL parameters, and all the helpers you use to read input and
write output. Every handler you write takes exactly one argument: a `*gin.Context`.

Say it to yourself once: **engine holds the routes, context handles the request.** With those two
ideas, the rest of Gin is just details.

```mermaid
flowchart LR
  E[gin.Engine<br/>holds the routes] --> R[route<br/>GET /ping]
  R --> H["handler<br/>func(c *gin.Context)"]
  H --> C[gin.Context<br/>reads input, writes output]
```

*One idea:* the engine matches an incoming request to a route, calls that route's handler, and the
handler uses the context to send a response. Every Gin endpoint you ever build flows along that arrow.

## Your first server

First, install Gin into your module. From inside your Go project:

```bash
go get github.com/gin-gonic/gin
```

*What just happened:* `go get` downloaded Gin and added it to your `go.mod`/`go.sum`. The import path
is `github.com/gin-gonic/gin`, and you refer to it in code as the `gin` package. That one command is
the whole install.

Now the smallest server that does something real. Create a file called `main.go`:

```go
package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})
	r.Run(":8080") // listens on :8080
}
```

*What just happened:* line by line - 
- `gin.Default()` creates the **engine** and returns a `*gin.Engine`. We name it `r` (for "router").
- `r.GET("/ping", ...)` registers a **route**: when a `GET` request arrives for the path `/ping`, run
  the function we pass. That function is the **handler**, and its signature - `func(c *gin.Context)` - 
  is the shape every Gin handler has.
- Inside the handler, `c.JSON(200, ...)` uses the **context** to write the response: it sets the
  HTTP status to `200`, sets the `Content-Type` to `application/json`, serializes the value to JSON,
  and sends it. Three chores, one call.
- `gin.H{"message": "pong"}` is the body. `gin.H` is Gin's shorthand for `map[string]any` - a quick
  way to build a JSON object without declaring a struct.
- `r.Run(":8080")` starts the server listening on port 8080. It blocks here, handling requests until
  you stop the program.

Run it like any Go program:

```bash
go run main.go
```

```console
$ go run main.go
[GIN-debug] Listening and serving HTTP on :8080
```

*What just happened:* `go run` compiled and started your program, and `r.Run` brought up the server.
Gin printed some startup logs (we trimmed them) and is now waiting for requests. Leave it running and,
in another terminal, hit the route:

```console
$ curl localhost:8080/ping
{"message":"pong"}
```

*What just happened:* `curl` sent a `GET /ping`. The engine matched it to your route, called your
handler, and the handler used the context to write back JSON. You have a working JSON API in twelve
lines of real code.

💡 `r.Run(addr)` is a small convenience over the standard library's `http.ListenAndServe` - same
result, less typing. With no argument, `r.Run()` defaults to `:8080`. Passing `":8080"` is just being
explicit about it.

## `gin.Default()` vs `gin.New()`

You'll see two ways to create the engine, and the difference is worth knowing on day one.

📝 **`gin.New()`** returns a *bare* engine - no extra behavior, just routing. **`gin.Default()`**
returns an engine with two pieces of **middleware** already attached: a **Logger** and a **Recovery**
handler. (Middleware is code that runs around every request; Phase 5 covers it properly.)

What those two give you:
- **Logger** prints a tidy line for every request - method, path, status code, and how long it took.
  That's the `[GIN]` output you saw scrolling by. It's how you *see* your server working.
- **Recovery** catches a panic inside any handler, turns it into a clean `500` response, and keeps
  the server alive. Without it, one panicking handler takes down the whole process.

```go
r := gin.Default() // Logger + Recovery, already wired
// vs.
r := gin.New()     // bare engine, you add what you want
```

*What just happened:* both give you a `*gin.Engine` you register routes on; `Default()` additionally starts
you with the two pieces of middleware almost every app wants. ⚠️ For learning and most apps, reach for
`gin.Default()`. Use `gin.New()` only when you deliberately want to control the middleware stack
yourself - otherwise you'll lose request logging and crash protection and wonder why your server
went quiet (or died).

## A second route, to make the flow stick

Engine → route → context → JSON. Add one more route and watch the same pattern repeat:

```go
func main() {
	r := gin.Default()

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})

	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok", "service": "tasks-api"})
	})

	r.Run(":8080")
}
```

*What just happened:* a second `r.GET` registered a second route on the same engine. A request to
`/health` runs its own handler, which builds a slightly bigger `gin.H` and sends it as JSON:

```console
$ curl localhost:8080/health
{"status":"ok","service":"tasks-api"}
```

Two routes, two handlers, one engine - and the context does the response work in both. Add a hundred
routes and it's the same idea a hundred times. That repetition is the point: once you've got the
shape, every endpoint is familiar.

## The running example: a tasks API

We won't keep writing throwaway `/ping` routes. Across this guide we'll grow one real service: a
small **tasks API**. The core of it is a single type - a task with an id, a title, and a done flag:

```go
type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}
```

*What just happened:* we declared the `Task` struct that the whole guide builds on. Those `json:"..."`
**struct tags** tell Gin what to call each field when it reads or writes JSON - so `Title` becomes
`"title"` in the response, not `"Title"`. (Tags do real work in both directions; binding incoming JSON
in Phase 3 leans on them too.) Here's the type returning itself through the now-familiar flow:

```go
r.GET("/tasks/sample", func(c *gin.Context) {
	t := Task{ID: 1, Title: "Read the Gin guide", Done: false}
	c.JSON(200, t)
})
```

*What just happened:* the handler built a `Task`, and `c.JSON` serialized it using those tags - no
`gin.H` needed when you already have a struct. Hit it and you get clean JSON back:

```console
$ curl localhost:8080/tasks/sample
{"id":1,"title":"Read the Gin guide","done":false}
```

By the end of the guide this grows into full create/read/update/delete over a real collection of
tasks. For now, you've met the cast: an **engine**, a **route**, a **handler**, a **context**, and
the **`Task`** we'll spend the next eight phases turning into a proper REST API. Next up: routing - 
path params, query params, wildcards, and grouping routes so they don't sprawl.

## Recap

- **Gin is a thin, fast layer over `net/http`.** Go can serve HTTP with just the stdlib, but Gin does
  the repetitive parts - routing, JSON, middleware - without hiding what's underneath.
- **The mental model is two things:** the **engine** (`*gin.Engine`) holds your routes; the
  **context** (`*gin.Context`) handles each request. Every handler is `func(c *gin.Context)`.
- **A first server is tiny:** `gin.Default()` makes the engine, `r.GET(path, handler)` registers a
  route, `c.JSON(200, ...)` writes the response, and `r.Run(":8080")` starts listening. Run with
  `go run main.go`, test with `curl`.
- **`gin.Default()` vs `gin.New()`:** `Default()` ships with Logger (per-request log lines) and
  Recovery (catch panics, return 500, stay alive). `New()` is bare. ⚠️ Prefer `Default()` unless you
  have a reason not to.
- **`gin.H` is `map[string]any`** - quick JSON without a struct. For real data, define a struct with
  `json:"..."` tags and pass it straight to `c.JSON`.
- **The throughline:** engine → route → handler → context → response. We'll grow one **tasks API**
  along that arrow for the rest of the guide.

## Quick check

Three questions on the ideas that have to stick - what Gin is, the engine/context split, and how a
first server fits together:

```quiz
[
  {
    "q": "What is the relationship between the engine (*gin.Engine) and the context (*gin.Context)?",
    "choices": [
      "The engine is the application that holds your routes; the context is handed to a handler for each request and carries the request and response helpers",
      "The engine and context are the same object with two names",
      "The context holds the routes and the engine handles each request",
      "The context is a database connection and the engine is the HTTP server"
    ],
    "answer": 0,
    "explain": "Engine holds the routes, context handles the request. You create one engine, register routes on it, and run it; each incoming request gets a *gin.Context with the request, response writer, params, and helpers. Every handler is func(c *gin.Context)."
  },
  {
    "q": "What does `gin.Default()` give you that `gin.New()` does not?",
    "choices": [
      "Logger and Recovery middleware already attached - per-request log lines, plus catching panics into a 500 instead of crashing",
      "A built-in database and ORM",
      "Faster routing because it compiles routes to machine code",
      "Automatic HTTPS certificates"
    ],
    "answer": 0,
    "explain": "gin.New() returns a bare engine. gin.Default() returns one with Logger (the per-request log lines) and Recovery (turns a handler panic into a clean 500 and keeps the server alive) already wired up. Most apps want Default()."
  },
  {
    "q": "In `c.JSON(200, gin.H{\"message\": \"pong\"})`, what is `gin.H`?",
    "choices": [
      "Shorthand for map[string]any, a quick way to build a JSON object without declaring a struct",
      "A required header that every Gin response must set",
      "The name of the HTTP handler function",
      "A special Gin type that connects to the database"
    ],
    "answer": 0,
    "explain": "gin.H is just an alias for map[string]any. It's a convenient way to assemble a JSON body inline. When you already have a struct (with json tags), you can pass it straight to c.JSON instead."
  }
]
```


---

# Routing & Route Groups

In Phase 1 you got a server running with a single route. Now we make it answer many requests, each one different. The whole job of a router is one decision, made very fast, on every request: *which handler runs?* Get that mental model and routing stops feeling like a pile of syntax and starts feeling like a lookup table you control.

## The mental model: method + path → handler

A route is a tiny rule with three parts: an **HTTP method**, a **path**, and a **handler**. When a request arrives, Gin reads the method and the path off it and finds the one route whose method and path match. That handler runs. Nothing else does.

```mermaid
flowchart LR
  A["GET /tasks/42"] --> B{Router}
  B -->|"GET /tasks"| C[list handler]
  B -->|"GET /tasks/:id"| D[one-task handler]
  B -->|"POST /tasks"| E[create handler]
```

> 📝 The same path with a different method is a *different* route. `GET /tasks` (list them) and `POST /tasks` (create one) live side by side, each with its own handler. That's not a quirk - it's how REST works, and Gin leans into it.

Two things make Gin's router pleasant. First, it's fast: under the hood it's a **radix tree** (think of it as a prefix-shared lookup tree), so matching stays quick even with hundreds of routes. Second, the path can contain **placeholders** - `/tasks/:id` matches `/tasks/42` *and* `/tasks/99` - so you don't register a route per task. We'll get there in a moment.

## Registering methods

Every HTTP method has a method on the engine. They all take the same two arguments: a path and a handler.

```go
package main

import "github.com/gin-gonic/gin"

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

var tasks = []Task{
	{ID: 1, Title: "Write the routing chapter", Done: true},
	{ID: 2, Title: "Ship the tasks API", Done: false},
}

func main() {
	r := gin.Default()

	r.GET("/tasks", func(c *gin.Context) {
		c.JSON(200, tasks)
	})

	r.POST("/tasks", func(c *gin.Context) {
		c.JSON(201, gin.H{"message": "created (we'll wire this up in Phase 3)"})
	})

	r.Run(":8080")
}
```

*What just happened:* We registered two routes that share the path `/tasks` but differ by method. `r.GET` answers list requests; `r.POST` answers create requests. `gin.H` is just Gin's shorthand for `map[string]interface{}` - a quick way to hand back a JSON object. The full set of method helpers is `r.GET`, `r.POST`, `r.PUT`, `r.PATCH`, `r.DELETE`, `r.HEAD`, and `r.OPTIONS`. There's also `r.Any(path, handler)` to match *every* method on one path, and `r.Handle(method, path, handler)` if you want to pass the method as a string.

Run it and try both:

```bash
go run main.go
# in another terminal:
curl http://localhost:8080/tasks
curl -X POST http://localhost:8080/tasks
```

> 💡 Reach for `r.Any` rarely. Being explicit about methods is a feature - it means a `DELETE` to a read-only endpoint gets a clean 405 instead of silently running your read handler.

## Path params: the `:name` placeholder

To fetch one task you need its id *from the URL*. That's what a path param is for. Put a colon before a segment name and Gin captures whatever sits there.

```go
r.GET("/tasks/:id", func(c *gin.Context) {
	id := c.Param("id") // a string, always

	for _, t := range tasks {
		if fmt.Sprintf("%d", t.ID) == id {
			c.JSON(200, t)
			return
		}
	}
	c.JSON(404, gin.H{"error": "task not found"})
})
```

*What just happened:* `:id` is a named slot. A request to `/tasks/2` makes `c.Param("id")` return the string `"2"`. We compare it against each task's id and return the match, or a 404 if nothing matches. The key detail: **`c.Param` always gives you a string** - the URL has no idea your ids are integers. When you need a real `int`, convert it yourself with `strconv.Atoi` (and handle the error, because someone *will* request `/tasks/banana`).

```bash
curl http://localhost:8080/tasks/2
# {"id":2,"title":"Ship the tasks API","done":false}
curl http://localhost:8080/tasks/999
# {"error":"task not found"}
```

> ⚠️ A path param matches exactly *one* segment. `/tasks/:id` matches `/tasks/2` but **not** `/tasks/2/comments` - that's a different route you'd register separately. When you genuinely need to match the rest of the path, you need a wildcard.

### Wildcards: matching the rest of the path

Sometimes the tail of the URL is open-ended - serving files, say, where the path can be `reports/q1/summary.pdf`. A `*name` segment captures everything from that point on.

```go
r.GET("/files/*filepath", func(c *gin.Context) {
	c.JSON(200, gin.H{"requested": c.Param("filepath")})
})
```

*What just happened:* `*filepath` is a catch-all. A request to `/files/reports/q1.pdf` sets `c.Param("filepath")` to `/reports/q1.pdf`. Two things to remember: it must be the **last** segment in the path, and **the captured value includes the leading slash**. Use it deliberately - for one id, `:id` is the right tool; the wildcard is for genuinely variable tails.

## Query params: everything after the `?`

Path params identify *which* resource. Query params tune *how* you want it - filters, pagination, search terms. They live after the `?` in the URL and Gin reads them off the context, never out of the path.

```go
r.GET("/tasks", func(c *gin.Context) {
	done := c.Query("done")            // "" if the param is absent
	page := c.DefaultQuery("page", "1") // falls back to "1"
	wantTags := c.QueryArray("tag")     // repeated ?tag=a&tag=b -> ["a", "b"]

	c.JSON(200, gin.H{
		"filter_done": done,
		"page":        page,
		"tags":        wantTags,
	})
})
```

*What just happened:* For a request to `/tasks?done=true&tag=work&tag=urgent`, `c.Query("done")` returns `"true"`, `c.DefaultQuery("page", "1")` returns `"1"` (since `page` wasn't sent), and `c.QueryArray("tag")` returns `["work", "urgent"]`. The three helpers cover the cases you actually hit: `c.Query` for an optional value (empty string when missing), `c.DefaultQuery` when absence has a sensible default, and `c.QueryArray` for a param that can repeat.

```bash
curl "http://localhost:8080/tasks?done=true&tag=work&tag=urgent"
# {"filter_done":"true","page":"1","tags":["work","urgent"]}
```

> 💡 Need to tell "absent" apart from "sent but empty" - `?q=` versus no `q` at all? Use `value, ok := c.GetQuery("q")`. The `ok` is `false` only when the param is truly missing, which matters for things like a search box that legitimately sends an empty string.

Like `c.Param`, every query helper returns a **string**. Converting `"true"` into a real `bool` or `"1"` into an `int` is your job - and that conversion-and-validation work is exactly what Phase 3 hands off to Gin's binding system, so you'll write less of it by hand soon.

## Route groups: stop repeating yourself

Real APIs version their URLs - `/api/v1/tasks`, `/api/v1/tasks/:id`, and so on. Typing `/api/v1` in front of every route is tedious and easy to get wrong. A **route group** is a shared path prefix: declare it once, register routes against the group, and Gin glues the prefix onto each.

```go
func main() {
	r := gin.Default()

	v1 := r.Group("/api/v1")
	{
		v1.GET("/tasks", listTasks)
		v1.GET("/tasks/:id", getTask)
		v1.POST("/tasks", createTask)
	}

	r.Run(":8080")
}
```

*What just happened:* `r.Group("/api/v1")` returns a group whose prefix is `/api/v1`. Every route registered on `v1` inherits that prefix, so `v1.GET("/tasks", ...)` actually serves `GET /api/v1/tasks`. The `{ }` braces are pure style - Go doesn't require them - but they visually bundle the group's routes together, which most Gin codebases do. Change the version in one place and every route moves with it.

Groups **nest**, too. You can group inside a group when one slice of your API needs a deeper prefix:

```go
v1 := r.Group("/api/v1")
admin := v1.Group("/admin")
admin.GET("/tasks", listAllTasks) // GET /api/v1/admin/tasks
```

*What just happened:* `admin` is a group built from `v1`, so its prefix stacks: `/api/v1` + `/admin`. The route ends up at `/api/v1/admin/tasks`. Prefixes compose exactly the way you'd hope - each level adds its piece.

There's one more reason groups matter, and it's the bigger one: a group can carry its own **middleware** with `v1.Use(...)`, so every route in that group runs the same auth check, logger, or rate limiter without you wiring it onto each handler. That's the whole point of grouping `/api/v1/admin` separately - it's where the "must be an admin" check lives. We'll build middleware properly in [Phase 5](05-middleware.md); for now, just hold the idea that *a group is a shared prefix and a shared pipeline*.

> ⚠️ **The classic startup panic.** Gin's radix-tree router won't let you register a static segment and a wildcard that conflict at the same spot. Declaring both `r.GET("/tasks/new", ...)` and `r.GET("/tasks/:id", ...)` is fine (Gin resolves `new` before falling back to `:id`), but mixing a wildcard with a param at the same level - like `/files/:name` and `/files/*path` together - **panics when the server starts**, not when a request arrives. The upside: you find out the instant you run it, not in production.

## Recap

- A route is **method + path → handler**; the same path under a different HTTP method is a separate route, each with its own handler.
- Every method has a helper (`r.GET`, `r.POST`, `r.PUT`, `r.PATCH`, `r.DELETE`, `r.HEAD`, `r.OPTIONS`), plus `r.Any` and `r.Handle`.
- Path params (`:id`, read with `c.Param("id")`) capture one segment; a wildcard (`*filepath`) captures the rest of the path and includes the leading slash. Both come back as strings.
- Query params come from after the `?`: `c.Query` (empty if absent), `c.DefaultQuery` (with a fallback), `c.QueryArray` (repeated values), and `c.GetQuery` (value + an `ok` for "was it sent?").
- A route group is a shared path prefix you declare once; groups nest and can carry their own middleware - the foundation for versioning like `/api/v1`.
- Conflicting wildcard/param routes at the same level panic at startup, so you catch the mistake immediately.

## Quick check

Test the mental model before moving on:

```quiz
[
  {
    "q": "A request comes in as POST /tasks. Which route handles it?",
    "choices": ["r.GET(\"/tasks\", ...)", "r.POST(\"/tasks\", ...)", "Both, in registration order", "Whichever was registered first"],
    "answer": 1,
    "explain": "A route is method + path. POST /tasks only matches the route registered with r.POST on that path; the GET route on the same path is a separate route."
  },
  {
    "q": "For a request to /tasks/42, what does c.Param(\"id\") return given the route /tasks/:id?",
    "choices": ["The integer 42", "The string \"42\"", "nil until you convert it", "An error you must handle"],
    "answer": 1,
    "explain": "Path params always come back as strings. If you need a real int, convert it yourself with strconv.Atoi and handle the error."
  },
  {
    "q": "You write v1 := r.Group(\"/api/v1\") then v1.GET(\"/tasks\", ...). What URL does that route serve?",
    "choices": ["/tasks", "/v1/tasks", "/api/v1/tasks", "/api/tasks"],
    "answer": 2,
    "explain": "A group prepends its prefix to every route registered on it, so v1.GET(\"/tasks\") serves GET /api/v1/tasks."
  }
]
```


---

# Binding & Validating Input

In Phase 2 you pulled values out of the URL one at a time - `c.Param("id")`, `c.Query("done")` - and got back strings you had to massage by hand. That's fine for one parameter. The moment a client POSTs a JSON body with five fields, doing it by hand turns into a pile of `c.GetRawData`, `json.Unmarshal`, and "is this field actually present?" checks. Gin has a better way, and you'll reach for it in almost every handler.

## The mental model: decode + validate, in one move

> 💡 **Binding** is one step that does two jobs: it *decodes* the incoming request (a JSON body, a query string, the URL path) onto a Go struct you define, and it *validates* that struct against rules you wrote as tags. After it succeeds, your handler works with normal, typed Go values - `in.Title` is a `string`, `in.Done` is a `bool` - not with raw bytes or `map[string]any`.

Think of the struct as a contract. You declare the shape you expect; binding either fills that shape with clean data or hands you an error explaining why it couldn't. Your handler logic never runs on half-parsed garbage, because you return early the instant binding fails.

That single idea - "describe the input as a struct, let Gin fill and check it" - is what this whole phase is about. Everything else is which method to call and which tags to write.

## `ShouldBindJSON` vs `BindJSON`: who handles the error?

Gin gives you two flavors of every binder, and the difference is one decision: *who writes the 400 response when the input is bad?*

- **`c.ShouldBindJSON(&obj)`** decodes and validates, then returns an `error` and **writes nothing**. If it fails, you decide the status code and the response body. This is the idiomatic choice - you stay in control.
- **`c.BindJSON(&obj)`** does the same decode and validate, but on failure it **automatically aborts the request with a `400` and a default error body**. Less typing, less control, and a response shape you didn't choose.

Reach for `ShouldBind*` by default. Here's the create-task handler for our tasks API, written the idiomatic way:

```go
type CreateTask struct {
    Title string `json:"title" binding:"required,min=1,max=120"`
    Done  bool   `json:"done"`
}

func create(c *gin.Context) {
    var in CreateTask
    if err := c.ShouldBindJSON(&in); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // From here on, in.Title and in.Done are clean, typed values.
    task := Task{ID: nextID(), Title: in.Title, Done: in.Done}
    c.JSON(http.StatusCreated, task)
}
```

*What just happened:* We declared a `CreateTask` struct describing the body we expect. `ShouldBindJSON(&in)` read the request body, unmarshalled it onto `in`, and checked the `binding` rules. If anything went wrong - malformed JSON, a missing `title`, a `title` over 120 characters - we got a non-nil `error`, returned `400` with a message *we* chose, and bailed before touching `in`. If it succeeded, the rest of the handler runs on trustworthy data.

> 📝 Note we bind onto a *separate* `CreateTask` struct, not directly onto our stored `Task{id, title, done}`. The input contract and the stored model are different things - the client doesn't get to set the `id`. Keeping them apart is a small habit that saves real bugs later.

## Struct tags: `json` names, `binding` validates

Two different tags do two different jobs on the same field, and mixing them up is a common early stumble:

- **`json:"title"`** tells the decoder *which JSON key maps to this field*. Without it, Gin matches case-insensitively on the field name, but being explicit is clearer and survives renames.
- **`binding:"required,min=1"`** tells the validator *what rules this field must satisfy*. Rules are comma-separated.

Gin's validation is powered by **[go-playground/validator v10](https://github.com/go-playground/validator)**, a mature library with a big rule vocabulary. The ones you'll actually use:

| Rule | Meaning |
|------|---------|
| `required` | Field must be present and non-zero |
| `email` | Must be a valid email address |
| `min` / `max` | String length, or numeric value, bounds |
| `gte` / `lte` | Numeric: greater/less than or equal |
| `oneof=a b c` | Must be exactly one of the listed values |
| `len` | Exact length |
| `numeric` | Must be a numeric string |

A richer input struct for the tasks API shows several at once:

```go
type CreateTaskRich struct {
    Title    string `json:"title" binding:"required,min=1,max=120"`
    Priority string `json:"priority" binding:"omitempty,oneof=low medium high"`
    Owner    string `json:"owner" binding:"omitempty,email"`
    Estimate int    `json:"estimate" binding:"gte=0,lte=40"`
}
```

*What just happened:* `Title` must be present and 1–120 characters. `Priority` is optional (`omitempty` skips validation when it's empty), but if supplied it must be exactly `low`, `medium`, or `high` - anything else is a `400`. `Owner` is optional but must look like an email when present. `Estimate` must land between 0 and 40 inclusive. All of that enforcement is declarative: no `if` statements in your handler, just tags.

## Binding query strings and URI params

JSON isn't the only thing you can bind. The same struct-driven approach works for the query string and for the path parameters from your routes - they just use different tags.

**Query string** uses `form` tags and `ShouldBindQuery`. This is the clean way to handle the list-with-filters endpoint:

```go
type ListQuery struct {
    Done  *bool `form:"done"`
    Limit int   `form:"limit" binding:"omitempty,gte=1,lte=100"`
}

func list(c *gin.Context) {
    var q ListQuery
    if err := c.ShouldBindQuery(&q); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    // q.Limit is an int; q.Done is *bool (nil = "not filtered").
}
```

*What just happened:* A request to `/tasks?done=true&limit=20` gets decoded onto `q` - Gin parses `"true"` into a real `bool` and `"20"` into a real `int`, with the `limit` bounds enforced. No more `strconv.Atoi` by hand. (We'll come back to why `Done` is a `*bool` in a second.)

**URI params** use `uri` tags and `ShouldBindUri`. Remember the `/tasks/:id` route from Phase 2 - here's how to bind and validate that `:id`:

```go
type TaskURI struct {
    ID int `uri:"id" binding:"required"`
}

func getOne(c *gin.Context) {
    var u TaskURI
    if err := c.ShouldBindUri(&u); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "id must be an integer"})
        return
    }
    // u.ID is an int parsed from the :id path segment.
}
```

*What just happened:* The `uri:"id"` tag wires the struct field to the `:id` route parameter. `ShouldBindUri` pulls the path segment and converts it to an `int`; a request to `/tasks/abc` fails the conversion and returns `400`, so your handler never sees a bad id. There's also **`c.ShouldBind`**, which picks the binder automatically based on the request's `Content-Type` (JSON body for `application/json`, form data otherwise) - handy when a handler accepts more than one input format.

## ⚠️ The zero-value gotcha

Here's the trap that bites everyone once. Go has no concept of "absent" for a plain `bool` or `int` - a missing field decodes to the type's **zero value**: `false` for `bool`, `0` for `int`, `""` for `string`.

That collides with validation in two ways:

```go
type UpdateTask struct {
    Done bool `json:"done" binding:"required"` // ⚠️ broken intent
}
```

*What just happened:* You wanted "`done` must be provided." But `required` rejects the **zero value**, and `false` *is* the zero value for `bool`. So a client sending `{"done": false}` - a perfectly valid, deliberate "mark it not done" - gets rejected as if the field were missing. `required` works well for strings and pointers (where empty/`nil` genuinely means absent); it does **not** distinguish "the client sent `false`" from "the client sent nothing" for a bare `bool`.

The fix when *absent* must differ from *false/0* is a **pointer**:

```go
type UpdateTask struct {
    Title *string `json:"title"` // nil = client didn't send it
    Done  *bool   `json:"done"`  // nil = absent; &false = explicit false
}

func update(c *gin.Context) {
    var in UpdateTask
    if err := c.ShouldBindJSON(&in); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    if in.Done != nil {
        // Client explicitly set done - apply *in.Done (could be false).
    }
    if in.Title != nil {
        // Client wants to change the title.
    }
}
```

*What just happened:* With `*bool`, a missing `done` decodes to `nil` and an explicit `{"done": false}` decodes to a pointer to `false`. Now you can tell them apart with a simple `nil` check - exactly what a PATCH-style partial update needs. The cost is a little pointer-dereferencing in the handler; the payoff is that "don't touch this field" and "set this field to false" stop being the same thing. When you don't need that distinction (a full create where every field is meant to be provided), plain values plus `required` on the strings is simpler and fine.

## Recap

- **Binding = decode + validate in one step**: describe the input as a struct, and Gin fills it with typed values and checks your rules, so handlers never run on raw or half-parsed data.
- **Prefer `ShouldBind*` over `Bind*`**: `ShouldBindJSON` returns an `error` and lets you choose the status and body; `BindJSON` auto-aborts with a default `400`.
- **Two tags, two jobs**: `json:"..."` (or `form:`/`uri:`) names the field; `binding:"..."` declares validation rules via go-playground/validator v10 (`required`, `email`, `min`/`max`, `oneof`, `gte`/`lte`, …).
- **One struct shape per source**: `ShouldBindJSON` for bodies, `ShouldBindQuery` (with `form` tags) for the query string, `ShouldBindUri` (with `uri` tags) for path params like `:id`.
- **Watch the zero value**: `required` rejects `false`/`0`/`""`, so it can't tell "absent" from "explicitly false" on bare `bool`/`int` - use pointers when that difference matters.

## Quick check

```quiz
[
  {
    "q": "What's the practical difference between c.ShouldBindJSON and c.BindJSON?",
    "choices": ["ShouldBindJSON validates but BindJSON does not", "ShouldBindJSON returns an error and writes nothing; BindJSON auto-aborts with a 400 on failure", "BindJSON is faster because it skips struct tags", "They are identical aliases for the same function"],
    "answer": 1,
    "explain": "ShouldBindJSON hands you the error so you control the response; BindJSON automatically aborts with a default 400."
  },
  {
    "q": "Which struct tag declares a validation rule (as opposed to naming the JSON field)?",
    "choices": ["json:\"title\"", "form:\"title\"", "binding:\"required,min=1\"", "uri:\"id\""],
    "answer": 2,
    "explain": "binding:\"...\" holds the go-playground/validator rules. json/form/uri just map a field to a source key."
  },
  {
    "q": "Why does binding:\"required\" on a plain bool field cause trouble for a 'mark as not done' update?",
    "choices": ["bool fields can't be bound at all", "required rejects the zero value, and false IS the zero value, so a deliberate {\"done\": false} is rejected as if absent", "required only works on query parameters", "bool fields always default to true"],
    "answer": 1,
    "explain": "A missing field and false both decode to the zero value, so required can't tell them apart. Use *bool when absent must differ from false."
  }
]
```


---

# Responses & Rendering

By now you can route a request and read what the client sent. This phase is the other half of the
conversation: writing the answer back.

Here's the mental model that keeps the whole topic small. The context (`*gin.Context`) is your **one
writer**. For each request you make exactly two decisions - *what status code* and *which render
helper* - and the helper does the rest: it sets the right `Content-Type` header, serializes your value,
and writes the bytes. `c.JSON` turns a struct into JSON. `c.String` writes plain text. `c.HTML` renders
a template. `c.File` streams a file from disk. Pick one, give it a status, and that's the entire
response.

> 📝 One response per request. Once you've called a render helper, the body is written and the status is
> locked in - you can't call `c.JSON` and then `c.String` for the same request. Decide, then write once.

## JSON: the helper you'll reach for 95% of the time

Most Gin services are JSON APIs, so `c.JSON` is the one to know cold. It takes a status code and any Go
value, marshals the value to JSON, sets `Content-Type: application/json`, and writes it.

The status code is the *other* half of a good response, and Gin makes you use real HTTP semantics for it.
Use the constants from the standard `net/http` package - `http.StatusOK` (200), `http.StatusCreated`
(201), `http.StatusNotFound` (404) - never bare numbers like `200`. The constants read as English and
stop typos.

We'll keep growing the **tasks API** from the earlier phases. A task is this struct:

```go
type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}
```

Here are the three response shapes you'll write over and over - a single item, a list, and the
two-status pattern (200 when found, 404 when not):

```go
package main

import (
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
)

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

var tasks = []Task{
	{ID: 1, Title: "Write the guide", Done: false},
	{ID: 2, Title: "Drink coffee", Done: true},
}

func main() {
	r := gin.Default()

	// A list of tasks → 200 OK
	r.GET("/tasks", func(c *gin.Context) {
		c.JSON(http.StatusOK, tasks)
	})

	// One task by id → 200 if found, 404 if not
	r.GET("/tasks/:id", func(c *gin.Context) {
		id, _ := strconv.Atoi(c.Param("id"))
		for _, t := range tasks {
			if t.ID == id {
				c.JSON(http.StatusOK, t)
				return
			}
		}
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
	})

	// Create a task → 201 Created, echo back the new resource
	r.POST("/tasks", func(c *gin.Context) {
		var in Task
		if err := c.ShouldBindJSON(&in); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		in.ID = len(tasks) + 1
		tasks = append(tasks, in)
		c.JSON(http.StatusCreated, in)
	})

	r.Run(":8080")
}
```

*What just happened:* Each handler ends in exactly one `c.JSON` call. `GET /tasks` returns the whole
slice as a JSON array. `GET /tasks/:id` returns the matched `Task` (200) or an error object (404) - note
the `return` after writing, so the loop doesn't fall through and try to write twice. `POST /tasks`
returns **201 Created** because a new resource was made, and echoes the created task back so the client
learns its assigned `ID`. That `gin.H{...}` is just Gin's shorthand for `map[string]interface{}` - a
quick way to build a small JSON object inline.

> 💡 Status codes carry meaning that clients, proxies, and your future monitoring all rely on. **201**
> for a successful create, **404** for a missing resource, **400** for bad input. Returning **200** for
> everything technically works but throws away free, standard signal.

A few JSON variants exist for when you need them, but reach for them rarely:

- `c.IndentedJSON(code, obj)` - pretty-printed with indentation. Handy for human-read debug endpoints; wasteful for production traffic.
- `c.PureJSON(code, obj)` - does **not** escape HTML characters like `<`, `>`, `&`. Plain `c.JSON` escapes them by default (safer); use `PureJSON` only when you specifically need the raw characters.
- `c.AsciiJSON(code, obj)` - escapes non-ASCII characters to `\uXXXX`, for transports that choke on UTF-8.

Default to `c.JSON`. The variants are there when a real requirement shows up, not before.

## The other response helpers

Not every response is JSON - the context has a writer for each common case, all following the same
"status + payload" shape:

```go
r.GET("/ping", func(c *gin.Context) {
	c.String(http.StatusOK, "pong")
})

r.GET("/greet/:name", func(c *gin.Context) {
	c.String(http.StatusOK, "Hello, %s!", c.Param("name"))
})

r.GET("/raw", func(c *gin.Context) {
	c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte("raw bytes here"))
})

r.GET("/report.pdf", func(c *gin.Context) {
	c.File("./files/report.pdf")
})

r.GET("/old-path", func(c *gin.Context) {
	c.Redirect(http.StatusFound, "/tasks")
})
```

*What just happened:* `c.String` writes plain text and accepts `fmt`-style formatting - `"%s"` gets
filled by the `c.Param("name")` argument, same as `fmt.Sprintf`. `c.Data` is the escape hatch: you
hand it the exact `Content-Type` and a `[]byte`, and it writes them verbatim - useful for content you've
already serialized or generated. `c.File` streams a file straight from disk and figures out the
content type from the extension. `c.Redirect` sends a 302 (`http.StatusFound`) with a `Location`
header pointing at `/tasks`; the browser follows it.

If you need to set a response header yourself, do it **before** the render helper writes the body:

```go
r.GET("/tasks.json", func(c *gin.Context) {
	c.Header("Cache-Control", "no-store")
	c.Header("X-Total-Count", strconv.Itoa(len(tasks)))
	c.JSON(http.StatusOK, tasks)
})
```

*What just happened:* `c.Header(key, value)` adds a header to the response. Once `c.JSON` runs, the
status and headers are flushed with the body, so setting headers afterward is too late. Order matters:
headers first, then the body. (There's also `c.Status(code)` if you want to set just the status with no
body - for example a `204 No Content` after a successful delete.)

## HTML templates: server-rendered pages

Sometimes you're not returning data - you're returning a *page*. Gin renders HTML using Go's standard
`html/template` package. The flow is two steps: load your templates into the engine once at startup, then
render one by name inside a handler.

Say you have a file `templates/tasks.tmpl`:

```html
<!DOCTYPE html>
<html>
<head><title>{{ .title }}</title></head>
<body>
  <h1>{{ .title }}</h1>
  <ul>
    {{ range .tasks }}
      <li>{{ .Title }} {{ if .Done }}(done){{ end }}</li>
    {{ end }}
  </ul>
</body>
</html>
```

Wire it up and render it:

```go
func main() {
	r := gin.Default()
	r.LoadHTMLGlob("templates/*.tmpl")

	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "tasks.tmpl", gin.H{
			"title": "My Tasks",
			"tasks": tasks,
		})
	})

	r.Run(":8080")
}
```

*What just happened:* `r.LoadHTMLGlob("templates/*.tmpl")` parses every matching template file once when
the server starts and registers each by its filename. (`r.LoadHTMLFiles("a.tmpl", "b.tmpl")` does the
same for an explicit list.) In the handler, `c.HTML` takes a status, the template **name**, and the data.
Inside the template, `{{ .title }}` reads the `"title"` key from the `gin.H` map, and `{{ range .tasks }}`
loops over the slice - note `.Title` and `.Done` are the struct's exported fields, so they're capitalized.

> ⚠️ `html/template` **auto-escapes** by default. If a task title were `<script>alert(1)</script>`, the
> template renders it as harmless text, not a running script - that's your built-in defense against XSS.
> Don't reach for tricks to disable escaping unless you fully understand the security cost; the safe
> default is doing real work for you.

## Static files: assets and built frontends

The last piece is files you don't generate per-request - CSS, images, JavaScript, fonts, or the compiled
output of a frontend build. You point a URL prefix at a directory and Gin serves whatever's inside:

```go
func main() {
	r := gin.Default()

	r.Static("/assets", "./assets")              // GET /assets/app.css → ./assets/app.css
	r.StaticFile("/favicon.ico", "./favicon.ico") // one specific file at a fixed path

	r.Run(":8080")
}
```

*What just happened:* `r.Static("/assets", "./assets")` maps the URL prefix `/assets` to the local
`./assets` directory - a request for `/assets/img/logo.png` serves `./assets/img/logo.png`, with content
types and caching handled for you. `r.StaticFile` is the single-file version, perfect for a favicon or a
`robots.txt` that lives at one exact URL. (There's also `r.StaticFS` if you're serving from an embedded
`fs.FS` rather than the real filesystem - common when you bundle assets into the binary.)

> 💡 In practice most Gin services are pure JSON APIs, and templates and static files barely come up. They
> matter for two cases: small server-rendered pages (an admin panel, a status page), or serving a built
> single-page app's files alongside its API from one binary. If you're building an API consumed by a
> separate frontend, you may never touch this section - and that's normal.

## Recap

- The context is your **one writer**: choose a status code and one render helper, and that's the whole response. One response per request.
- `c.JSON(status, value)` is the workhorse - it marshals, sets `Content-Type`, and writes. Use it for nearly everything.
- Use `net/http` status constants with meaning: **201** on create, **404** when missing, **400** for bad input - not 200 for everything.
- Other helpers cover the rest: `c.String` (text, with formatting), `c.Data` (raw bytes), `c.File` (stream from disk), `c.Redirect` (302 + Location). Set headers with `c.Header` *before* writing the body.
- `r.LoadHTMLGlob` + `c.HTML` render server-side pages with Go's `html/template`, which auto-escapes to block XSS by default.
- `r.Static` / `r.StaticFile` serve assets and built frontends - but most real services are JSON-only and rarely need them.

## Quick check

Test the two decisions every response comes down to:

```quiz
[
  {
    "q": "A handler successfully creates a new task. Which response is most correct?",
    "choices": ["c.JSON(http.StatusOK, task)", "c.JSON(http.StatusCreated, task)", "c.String(http.StatusOK, \"created\")", "c.Status(http.StatusNoContent)"],
    "answer": 1,
    "explain": "A successful create should return 201 Created (http.StatusCreated) and echo the new resource so the client learns its assigned ID."
  },
  {
    "q": "You want to add an X-Total-Count header to a JSON response. When must you call c.Header?",
    "choices": ["After c.JSON, since the body comes first", "Before c.JSON, because the render helper flushes status and headers with the body", "It doesn't matter, order is irrelevant", "Only inside middleware, never in a handler"],
    "answer": 1,
    "explain": "Render helpers write the status, headers, and body together. Set headers before the body is written, or they're too late."
  },
  {
    "q": "Why does c.HTML render a task title of \"<script>alert(1)</script>\" as visible text instead of running it?",
    "choices": ["Gin strips all HTML tags from data", "Go's html/template auto-escapes output by default", "Browsers ignore scripts inside <li>", "You must manually call c.Escape first"],
    "answer": 1,
    "explain": "html/template auto-escapes interpolated values by default, which is your built-in XSS protection."
  }
]
```


---

# Middleware

Here's the thing nobody tells you up front: most of the "framework" part of a web framework isn't the routing - it's the stuff that runs *around* every request. Logging, auth checks, panic recovery, timing, CORS headers. You don't want to paste those into all forty of your handlers. You want to write them once and have them wrap everything.

That wrapper is **middleware**, and in Gin it's the same shape as a handler. If you can picture a handler, you already understand 90% of middleware.

## The mental model: a handler that runs around other handlers

> 💡 Middleware is a handler that runs *around* your route handler. Think of it as an onion: the request travels inward through each layer, hits your handler in the center, and then travels back outward through those same layers.

The seam between "going in" and "coming back out" is one function call: **`c.Next()`**. Code you write *before* `c.Next()` runs on the way in. Code *after* it runs on the way out, after the handler (and everything deeper) has finished. That single fact unlocks every middleware pattern there is.

```mermaid
flowchart LR
  R[Request] --> L1[Logger: before]
  L1 --> A1[Auth: before]
  A1 --> H[Your handler]
  H --> A2[Auth: after]
  A2 --> L2[Logger: after]
  L2 --> Resp[Response]
```

Once you see it as "a chain of handlers with a seam in the middle," the rest is mechanics.

## The signature, and the three ways to register

A middleware *is* a `gin.HandlerFunc` - the exact same `func(c *gin.Context)` your route handlers are. There's no special type, no magic interface. The only convention is that middleware usually calls `c.Next()` (or `c.Abort()`) somewhere.

The common pattern is a function that *returns* a `gin.HandlerFunc`. That outer function is where you do one-time setup (read config, open a logger) and the returned closure is what runs per request:

```go
func Hello() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Println("a request came in")
        c.Next()
    }
}
```

*What just happened:* `Hello()` runs once, when you register it. The inner `func(c *gin.Context)` it hands back runs on every request. This "function returning a HandlerFunc" shape is the idiom you'll see everywhere - get comfortable with it now.

You attach middleware in one of three scopes:

```go
r := gin.New()

// 1. Global - runs for EVERY route on this engine.
r.Use(Hello())

// 2. Per-group - runs only for routes in this group.
v1 := r.Group("/api/v1")
v1.Use(Hello())

// 3. Per-route - runs only for this one route. List middleware before the handler.
r.GET("/admin", Hello(), adminHandler)
```

*What just happened:* same middleware, three reaches. `r.Use` wraps the whole app; `group.Use` wraps a subtree of routes (this is how you protect `/api/v1/*` without touching public routes); and listing it inline on `r.GET` wraps exactly one endpoint. Pick the narrowest scope that does the job.

> 📝 Order matters. Middleware runs in the order you register it. `r.Use(A); r.Use(B)` means A's "before" code runs first and A's "after" code runs *last* (outermost layer of the onion).

## `c.Next()`, and how to stop the chain with `c.Abort()`

You don't actually have to call `c.Next()` yourself in most middleware - Gin calls the next handler automatically when yours returns. You call `c.Next()` explicitly when you have work to do *after* the rest of the chain finishes. The classic example is timing a request: you note the start, let everything downstream run, then measure on the way out.

```go
func Timer() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        log.Printf("%s %s took %v", c.Request.Method, c.FullPath(), time.Since(start))
    }
}
```

*What just happened:* `start := time.Now()` runs on the way in. `c.Next()` hands control to the rest of the chain - the actual handler runs, writes its response, and returns. Only *then* does the `log.Printf` line run, with `time.Since(start)` capturing the full request duration. Without the explicit `c.Next()`, you'd have no "after" point to measure from. This is also exactly how you'd set a response header *after* the handler decides what it's doing.

The other direction is stopping the chain early. When a middleware decides the request shouldn't continue - failed auth, rate limit hit - it calls **`c.Abort()`**. Abort means "no handler after me in this chain will run." Note: `c.Abort()` does *not* write a response by itself; it just halts the chain. Almost always you want **`c.AbortWithStatusJSON(code, obj)`**, which aborts *and* writes a JSON error in one call.

```go
func RequireHeader() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.GetHeader("X-Demo") == "" {
            c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing X-Demo header"})
            return
        }
        c.Next()
    }
}
```

*What just happened:* if the header is missing, we abort with a 400 and a JSON body - the handler never runs. The bare `return` after the abort is important: `c.AbortWithStatusJSON` flags the chain as stopped, but your own function keeps executing until it returns, so you `return` to avoid running the rest of *this* middleware's code. If the header is present, `c.Next()` lets the request through.

> ⚠️ Forgetting the `return` after an abort is the #1 middleware bug. Without it, your middleware writes the error response *and then keeps going*, often writing a second response and crashing with "headers already written." Abort, then return.

## Passing data down the chain: `c.Set` and `c.Get`

Middleware often needs to hand something to the handler - most commonly, *who the user is* after authenticating them. The context carries a small per-request key/value store for exactly this: `c.Set("key", value)` stashes it, and `c.Get("key")` reads it back later (returning the value and an `ok` bool). There's also `c.MustGet("key")` when you're certain it's there.

Let's wire a real **auth middleware** onto the `/api/v1` group from Phase 2. It reads an `Authorization` header, rejects the request with a 401 if it's missing, and otherwise records the current user for downstream handlers:

```go
func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing Authorization header"})
            return
        }

        // In a real app you'd verify the token here. We'll fake a lookup.
        user := lookupUser(token) // returns "" if the token is invalid
        if user == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            return
        }

        c.Set("user", user) // hand the user to downstream handlers
        c.Next()
    }
}
```

*What just happened:* two abort paths (no header, bad token) and one success path. On success we `c.Set("user", user)` and `c.Next()`. The handler can now read that user without re-doing any auth work - the middleware is the single place that knows how to authenticate.

Now apply it to the tasks group and read the user inside a handler:

```go
func main() {
    r := gin.Default()

    v1 := r.Group("/api/v1")
    v1.Use(Auth()) // every route under /api/v1 now requires a valid token

    tasks := v1.Group("/tasks")
    tasks.GET("", listTasks)

    r.Run(":8080")
}

func listTasks(c *gin.Context) {
    user := c.MustGet("user").(string) // we KNOW Auth ran and set this
    c.JSON(http.StatusOK, gin.H{
        "user":  user,
        "tasks": []string{"write tests", "ship it"},
    })
}
```

*What just happened:* because `Auth()` is on the `v1` group, the `/api/v1/tasks` routes are all protected - no token, no entry. Inside `listTasks`, `c.MustGet("user")` retrieves what the middleware stashed; the `.(string)` is a type assertion because the store holds `any`. `MustGet` panics if the key is missing, which is fine *here* because the middleware guarantees it ran first. If you weren't certain, you'd use `user, ok := c.Get("user")` and check `ok`.

Try it: `curl localhost:8080/api/v1/tasks` gets a 401, while `curl -H "Authorization: valid-token" localhost:8080/api/v1/tasks` gets the list.

## The built-ins: Logger and Recovery

You've been using `gin.Default()` this whole guide, and it's been quietly attaching two middlewares for you:

- **`gin.Logger()`** - logs every request (method, path, status, latency) to stdout. It's the colored line you see in your terminal on each request.
- **`gin.Recovery()`** - catches any panic in your handlers, logs the stack trace, and returns a 500 instead of crashing the whole server. Without it, one panic in one handler takes down your process.

```go
// These two lines are equivalent:
r := gin.Default()

r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
```

*What just happened:* `gin.Default()` is literally `gin.New()` plus those two `Use` calls. That's the entire difference. `gin.New()` gives you a bare engine with *no* middleware - useful when you want full control (say, a custom structured JSON logger instead of Gin's default, or no logging at all in a high-throughput service).

> ⚠️ If you switch to `gin.New()`, you almost certainly still want `gin.Recovery()`. Dropping the logger is a reasonable choice; dropping panic recovery means a single nil-pointer dereference in any handler kills your server for *every* user. Add Recovery back unless you have a deliberate reason not to.

## Recap

- Middleware is a `gin.HandlerFunc` - the same `func(c *gin.Context)` as a handler - that runs *around* your handlers. The idiom is a function returning that closure.
- Register it three ways: `r.Use()` (global), `group.Use()` (a subtree, e.g. `/api/v1`), or inline on a route (`r.GET("/x", mw, handler)`). Order of registration is order of execution.
- `c.Next()` is the seam: code before it runs on the way in, code after runs on the way out. Use it to time requests or set response headers post-handler.
- `c.Abort()` stops the chain (remember to `return`); `c.AbortWithStatusJSON(code, obj)` stops it *and* writes a response - your go-to for auth and validation failures.
- `c.Set`/`c.Get` (and `c.MustGet`) pass per-request data down the chain; the canonical use is an auth middleware setting the current user.
- `gin.Default()` = `gin.New()` + `gin.Logger()` + `gin.Recovery()`. `gin.New()` has neither - keep Recovery unless you really mean to drop it.

## Quick check

Lock in the one idea that matters most - the `c.Next()` seam and aborting:

```quiz
[
  {
    "q": "In a middleware, where does code placed AFTER c.Next() run?",
    "choices": ["Before the request reaches the handler", "After the handler (and rest of the chain) has finished, on the way out", "It never runs", "Only if the handler calls c.Next() again"],
    "answer": 1,
    "explain": "c.Next() is the seam: code before it runs on the way in, code after it runs on the way out - which is why Timer() can measure total request duration there."
  },
  {
    "q": "An auth middleware rejects a request. Which call both stops the chain AND writes a JSON error response?",
    "choices": ["c.Next()", "c.Abort()", "c.AbortWithStatusJSON(401, obj)", "c.Set(\"error\", obj)"],
    "answer": 2,
    "explain": "c.Abort() only halts the chain without writing anything; c.AbortWithStatusJSON aborts and writes the response in one call. (Don't forget to return after it.)"
  },
  {
    "q": "What is the difference between gin.Default() and gin.New()?",
    "choices": ["Default() is faster", "Default() adds gin.Logger() and gin.Recovery(); New() adds neither", "New() adds Logger and Recovery; Default() adds neither", "They are identical"],
    "answer": 1,
    "explain": "gin.Default() is just gin.New() with Logger and Recovery attached via Use(). New() is the bare engine - keep Recovery() if you switch to it."
  }
]
```


---

# Building a REST API

This is the phase where it all comes together. For five phases you've collected the pieces - the engine and its routes, route groups, binding JSON onto structs, validation tags, `c.JSON` and status codes, middleware. None of those were ends in themselves; they were parts for *this*: a real REST API you can hit with `curl` and watch behave like the services you'll build at work.

We're going to grow the tasks API from "a few scattered handlers" into one complete, coherent resource. By the end you'll have create, read, update, and delete all wired up - and, more importantly, a mental model that makes the next resource you build feel like filling in a template.

## The mental model: a resource is five handlers over one collection

> 💡 A REST **resource** is a *collection of things* plus the five standard operations you can do to it. For our tasks, that's: **list** them all, **get** one by id, **create** a new one, **update** an existing one, and **delete** one. That's the whole shape. Five handlers, one collection. Every resource you ever build - users, orders, invoices, comments - is the same five verbs over a different noun.

Those five operations map onto HTTP methods and paths so predictably that the mapping is practically a law:

| Operation | Method & path | Success status |
|-----------|---------------|----------------|
| List all | `GET /tasks` | `200 OK` |
| Get one | `GET /tasks/:id` | `200 OK` (or `404`) |
| Create | `POST /tasks` | `201 Created` |
| Update | `PUT /tasks/:id` | `200 OK` (or `404`) |
| Delete | `DELETE /tasks/:id` | `204 No Content` (or `404`) |

Notice the symmetry: the *collection* (`/tasks`) is where you list and create; a *single item* (`/tasks/:id`) is where you get, update, and delete. Once you see that, you're not memorizing five unrelated functions - you're filling in a known grid. We'll state the grid, then build it cell by cell.

## The store: shared state, guarded

Before handlers, we need somewhere to keep tasks. We'll use an in-memory store - a plain Go map behind a counter. But there's a trap here that catches people who came from single-threaded backgrounds, so let's name it before we write the code.

> ⚠️ Gin handles requests **concurrently**. Every incoming request runs its handler in its own goroutine, and two of them can hit your map at the *exact same moment*. Go maps are not safe for concurrent read+write - a concurrent map write will crash your program with a `fatal error: concurrent map writes` (and it won't be a recoverable panic; it kills the process). Any state shared across requests must be guarded. We'll use a `sync.RWMutex`: many readers at once, one writer alone.

```go
package main

import (
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

type store struct {
	mu     sync.RWMutex
	tasks  map[int]Task
	nextID int
}

func newStore() *store {
	return &store{
		tasks:  make(map[int]Task),
		nextID: 1,
	}
}
```

*What just happened:* `Task` is our stored model - `ID`, `Title`, `Done`, each with a `json` tag so it serializes with lowercase keys. The `store` bundles three things that belong together: the `map[int]Task` of tasks keyed by id, a `nextID` counter for assigning fresh ids, and an `RWMutex` that guards both. `newStore` hands back a ready-to-use store with an initialized map (a nil map panics on write) and the counter starting at 1. Every handler will reach for the mutex before touching `tasks` or `nextID` - that discipline is what keeps concurrent requests from corrupting each other.

> 📝 An `RWMutex` distinguishes read locks (`RLock`/`RUnlock`) from write locks (`Lock`/`Unlock`). Reads can overlap each other freely; a write blocks everyone until it's done. For a read-heavy API that's a nice fit. If this feels like overkill for now, a plain `sync.Mutex` (one lock for everything) would also be correct - just less concurrent on reads.

## The five handlers

We'll hang the handlers as methods on `*store`, so each one has direct access to the map and the lock. Method-on-store keeps the wiring tidy and means we don't reach for package-level globals. Here they are, one per operation.

### List - `GET /tasks`

```go
func (s *store) list(c *gin.Context) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]Task, 0, len(s.tasks))
	for _, t := range s.tasks {
		out = append(out, t)
	}
	c.JSON(http.StatusOK, out)
}
```

*What just happened:* We took a *read* lock (`RLock`) because we're only looking, `defer`-ing the unlock so it releases no matter how the function exits. We copy the map's values into a slice and return it with `200`. The `make([]Task, 0, ...)` detail matters: an empty slice serializes to `[]`, but a `nil` slice serializes to `null` - and clients much prefer an empty array to a surprise `null`. Pre-sizing with `len(s.tasks)` is a small efficiency, not a requirement.

### Get one - `GET /tasks/:id`

```go
func (s *store) getOne(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id must be an integer"})
		return
	}

	s.mu.RLock()
	t, ok := s.tasks[id]
	s.mu.RUnlock()

	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	c.JSON(http.StatusOK, t)
}
```

*What just happened:* We pulled `:id` from the path with `c.Param("id")` and converted it with `strconv.Atoi` - a non-numeric id like `/tasks/abc` fails the conversion and earns a `400` before we ever touch the store. Then a quick read-locked map lookup. The `t, ok := s.tasks[id]` comma-ok idiom is the whole game: `ok` is `false` when the key is absent, which is exactly our `404` case. Found means `200` with the task. (You could bind `:id` with `ShouldBindUri` as in Phase 3; `strconv.Atoi` is the lighter-weight choice for a single param.)

### Create - `POST /tasks`

```go
type CreateTask struct {
	Title string `json:"title" binding:"required,min=1,max=120"`
	Done  bool   `json:"done"`
}

func (s *store) create(c *gin.Context) {
	var in CreateTask
	if err := c.ShouldBindJSON(&in); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	s.mu.Lock()
	id := s.nextID
	s.nextID++
	t := Task{ID: id, Title: in.Title, Done: in.Done}
	s.tasks[id] = t
	s.mu.Unlock()

	c.JSON(http.StatusCreated, t)
}
```

*What just happened:* This is Phase 3's binding plus Phase 4's responses, fused. We bind onto a separate `CreateTask` input struct - the client doesn't get to set the `id`, so the input contract differs from the stored `Task`. Binding fails on bad input and returns `400` with the validator's message. On success we take a *write* lock (`Lock`, not `RLock` - we're mutating), grab and bump `nextID`, build the `Task`, and store it. Crucially, both the id-bump and the map-write happen inside one lock, so two simultaneous creates can never grab the same id. We return `201 Created` with the new task, id and all, so the client learns what id it got.

### Update - `PUT /tasks/:id`

```go
func (s *store) update(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id must be an integer"})
		return
	}

	var in CreateTask
	if err := c.ShouldBindJSON(&in); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.tasks[id]; !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	t := Task{ID: id, Title: in.Title, Done: in.Done}
	s.tasks[id] = t
	c.JSON(http.StatusOK, t)
}
```

*What just happened:* `PUT` is "replace the whole thing at this id," so we do both jobs the create and get-one did: parse `:id`, then bind the new body. We reuse `CreateTask` as the input shape since a full replace wants the same fields. Under a write lock, we check the task exists - `404` if it doesn't, because `PUT` to a missing id is a not-found, not a silent create here - then overwrite it with a fresh `Task` carrying the original `id`. We return the updated task with `200`. Note the `defer s.mu.Unlock()` this time: with two early-return paths inside the lock, `defer` is the safe way to guarantee the unlock fires on every branch.

### Delete - `DELETE /tasks/:id`

```go
func (s *store) remove(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "id must be an integer"})
		return
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.tasks[id]; !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	delete(s.tasks, id)
	c.Status(http.StatusNoContent)
}
```

*What just happened:* We parse and validate the id, take a write lock, and confirm the task exists - `404` if not, so a delete tells you plainly whether there was anything to delete. If it's there, Go's built-in `delete` removes the key, and we reply `204 No Content`. We use `c.Status` rather than `c.JSON` because `204` means "success, and there's deliberately no body" - sending JSON with a `204` is contradictory. (We named the method `remove`, not `delete`, because `delete` is a Go built-in and shadowing it would be asking for confusion.)

## Wiring it to a route group

Handlers do nothing until they're registered. Here's `main`, mapping each handler to its method and path inside a versioned group - the `/api/v1` prefix from Phase 2, so a future `/api/v2` can live alongside it without breaking clients.

```go
func main() {
	r := gin.Default()
	s := newStore()

	v1 := r.Group("/api/v1")
	{
		v1.GET("/tasks", s.list)
		v1.POST("/tasks", s.create)
		v1.GET("/tasks/:id", s.getOne)
		v1.PUT("/tasks/:id", s.update)
		v1.DELETE("/tasks/:id", s.remove)
	}

	r.Run(":8080")
}
```

*What just happened:* `gin.Default()` gives us an engine with the Logger and Recovery middleware from Phase 5 already attached. We create one `store` and share it across all handlers - that single shared state is exactly why the mutex earned its keep. `r.Group("/api/v1")` returns a group whose routes all carry the `/api/v1` prefix; the `{ }` braces are just a Go block for visual grouping (they have no special meaning to Gin, but they read nicely). Each `v1.METHOD(path, handler)` call binds one cell of our five-cell grid. `r.Run(":8080")` starts serving. That's the entire API - five lines of routing over a store and five handlers.

> 📝 The full paths are `/api/v1/tasks` and `/api/v1/tasks/:id`. Gin routes `GET /api/v1/tasks` and `GET /api/v1/tasks/:id` to different handlers even though they share a prefix, because the trailing `/:id` segment distinguishes them - that's the router doing exactly what Phase 2 promised.

## Driving it with curl

Theory's done. Let's hit the running server and watch the grid behave. Start it with `go run .`, then in another terminal:

**Create a task:**

```bash
curl -s -X POST localhost:8080/api/v1/tasks \
  -H 'Content-Type: application/json' \
  -d '{"title": "write the phase 6 guide"}'
```

```json
{"id":1,"title":"write the phase 6 guide","done":false}
```

*What just happened:* We POSTed a JSON body with just a `title`. Binding filled `Title`, left `Done` at its zero value (`false`), the store assigned `id: 1`, and we got `201` with the created task - including the id we now know to use for the next calls.

**Create another, then list them all:**

```bash
curl -s -X POST localhost:8080/api/v1/tasks \
  -H 'Content-Type: application/json' \
  -d '{"title": "ship it", "done": true}'

curl -s localhost:8080/api/v1/tasks
```

```json
[{"id":1,"title":"write the phase 6 guide","done":false},{"id":2,"title":"ship it","done":true}]
```

*What just happened:* The second create got `id: 2` from the counter. The `GET /api/v1/tasks` returned both as a JSON array with `200`. (Map iteration order in Go is randomized, so the order across calls isn't guaranteed - if you need a stable order, sort the slice before returning it.)

**Get one by id, then delete it:**

```bash
curl -s localhost:8080/api/v1/tasks/1

curl -s -i -X DELETE localhost:8080/api/v1/tasks/1
```

```json
{"id":1,"title":"write the phase 6 guide","done":false}
```

```
HTTP/1.1 204 No Content
```

*What just happened:* The `GET /tasks/1` returned task 1 with `200`. The `DELETE /tasks/1` returned `204` with an empty body - we used `-i` to show the status line, since there's no body to print. Ask for it again with `curl localhost:8080/api/v1/tasks/1` now and you'll get `404 {"error":"task not found"}`, because it's gone.

## The store is a stand-in

> 💡 Look back at the five handlers and notice what they *don't* depend on: nothing in them cares that the data lives in a map. They take input, validate it, call `s.something(id)`, and shape a response. That map is a placeholder for a real database. When you later swap it for [GORM](/guides/gorm-from-zero) talking to Postgres, the handler bodies barely change - `s.tasks[id]` becomes `db.First(&task, id)`, `s.tasks[id] = t` becomes `db.Save(&t)`, and the `RWMutex` disappears entirely because the database handles concurrency for you. The routing, binding, validation, status codes, and response shapes - everything this phase built - stay exactly as they are. That's the payoff of keeping the store behind a small interface in your head: the web layer and the data layer are separable.

## Recap

- **A resource is five handlers over one collection**: list (`GET /tasks`), get-one (`GET /tasks/:id`), create (`POST`), update (`PUT`), delete (`DELETE`) - a grid you fill in, not five unrelated functions.
- **Shared state must be guarded**: Gin runs handlers concurrently, so any state touched by multiple requests needs a `sync.Mutex`/`RWMutex` - read-lock for lookups, write-lock for mutations - or your map will crash the process.
- **Match status codes to operations**: `200` for reads and updates, `201` for create (return the new item with its id), `204` with no body for delete, `404` when an id isn't found, `400` when input is bad.
- **Keep input and stored models separate**: bind onto a `CreateTask` struct so the client can't set the `id`; the stored `Task` carries the id the server assigns.
- **The store is a database stand-in**: handlers depend on operations, not on the map - swap in GORM later and the routing, binding, and responses stay put.

## Quick check

```quiz
[
  {
    "q": "Why does the in-memory store need a sync.Mutex (or RWMutex)?",
    "choices": ["To make handlers run faster", "Because Gin handles requests concurrently and concurrent map read+write crashes the program", "Because Go maps require a lock to be created", "To enable JSON serialization of the map"],
    "answer": 1,
    "explain": "Gin runs each request in its own goroutine. Two goroutines writing the same map concurrently triggers a fatal 'concurrent map writes' error, so shared state must be guarded."
  },
  {
    "q": "Which status code does the create handler (POST /tasks) return on success, and why?",
    "choices": ["200 OK, because the request succeeded", "204 No Content, because nothing was returned", "201 Created, because a new resource was made and the new task (with its id) is returned", "302 Found, to redirect to the new task"],
    "answer": 2,
    "explain": "201 Created is the standard for a successful POST that makes a new resource, and returning the created task lets the client learn the assigned id."
  },
  {
    "q": "Why does the delete handler use c.Status(http.StatusNoContent) instead of c.JSON?",
    "choices": ["Because c.JSON does not support 204", "Because 204 means success with deliberately no body, so sending JSON would contradict it", "Because delete is faster without serialization", "Because the deleted task must not be revealed"],
    "answer": 1,
    "explain": "204 No Content signals success with no response body. Attaching a JSON body to a 204 is contradictory, so c.Status is the right call."
  }
]
```


---

# Error Handling & Project Structure

By Phase 6 the tasks API works: create a task, list them, fetch one, update, delete. But look closely
and you'll notice the handlers have quietly turned into a mess. Each one writes errors in its own little
dialect - one returns `gin.H{"error": "..."}`, another `gin.H{"message": "..."}`, a third forgets the
status code and leans on Gin's default. The business logic - "does this task exist?", "is the title
empty?" - is tangled up with the HTTP plumbing. It runs, but you wouldn't want to add a tenth route.

This phase fixes both problems at once, because they're the same problem wearing two hats.

## The mental model: a handler is a translator

Here's the idea to hold onto before any code:

> 💡 **A handler translates between HTTP and your domain - and nothing more.** It reads the request
> (params, JSON body), hands the *meaning* down to your logic, takes back a result or an error, and
> translates that into a status code and a JSON body. The decisions - "this title is invalid," "no task
> has that id" - happen *below* the handler, in plain Go that knows nothing about HTTP.

When you internalize that, two things follow naturally. First, handlers get short and boring (a good
thing). Second, errors stop being a per-handler improvisation: your logic raises a plain Go `error`, and
*one* place turns every error into *one* consistent JSON shape. A client should be able to write
`response.error` once and have it work for every endpoint in your API. That consistency is not a nicety - 
it's the difference between an API people enjoy and one they reverse-engineer.

The rest of this phase builds toward that, in layers:

1. The basics - returning errors from inside one handler (and the `return` you must not forget).
2. Centralizing - `c.Error` plus a middleware that writes the single error shape.
3. Mapping meaning to status - sentinel errors in a service layer, matched with `errors.Is`.
4. Structure - splitting the one big file so each layer has a home.

## 1. Per-handler errors, and the `return` that bites everyone

The simplest way to report an error is right where you find it. You write a JSON body with a status code
and stop. Here's a handler that fetches one task by id from an in-memory store:

```go
func getTask(c *gin.Context) {
    id := c.Param("id")

    task, ok := tasks[id]
    if !ok {
        c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
        return
    }

    c.JSON(http.StatusOK, task)
}
```

*What just happened:* if the id isn't in the map, we write a `404` with an error body - and then `return`.
That `return` is doing real work, and forgetting it is the single most common Gin bug.

> ⚠️ **`c.JSON` does not stop your handler.** Writing a response does not end the function - Go keeps
> running the next lines. Without the `return`, this handler would write the `404` body *and then* fall
> through to `c.JSON(http.StatusOK, task)`, trying to write a second response. The status is already sent,
> so Gin logs a `headers already written` warning and the client gets a garbled body. Every error branch
> that writes a response must be followed by `return`.

There's a second helper that bundles "write and stop" into one call:

```go
func getTask(c *gin.Context) {
    id := c.Param("id")

    task, ok := tasks[id]
    if !ok {
        c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "task not found"})
        return
    }

    c.JSON(http.StatusOK, task)
}
```

*What just happened:* `AbortWithStatusJSON` writes the body **and** sets the abort flag on the context, so
no *later middleware* in the chain runs its post-`c.Next()` code as if the request succeeded. Note the
`return` is still there - abort flips a flag, it doesn't perform a Go `return` for you. Use
`AbortWithStatusJSON` when you specifically want to halt the middleware chain (auth failures, rate limits);
plain `c.JSON` + `return` is fine for ordinary "not found" type errors inside the final handler.

So far so good - but if you write this in ten handlers, you've hand-rolled the error shape ten times.
That's the problem the next two layers solve.

## 2. Centralized errors: `c.Error` + an error middleware

Gin gives the context a small superpower: every `*gin.Context` carries a slice of errors, `c.Errors`. You
can *attach* an error to it without writing any response:

```go
func getTask(c *gin.Context) {
    id := c.Param("id")

    task, ok := tasks[id]
    if !ok {
        c.Error(errors.New("task not found"))  // attaches; writes nothing
        c.Status(http.StatusNotFound)
        return
    }

    c.JSON(http.StatusOK, task)
}
```

*What just happened:* `c.Error(err)` appends the error onto `c.Errors` and returns - it does **not** touch
the response body. The handler's job shrinks to "decide the status, attach the error, get out." Something
else will turn that attached error into JSON. That "something else" is a middleware.

A middleware can run code *after* the handler by calling `c.Next()` first, then inspecting what the handler
left behind. So we write an `ErrorHandler` that, once the chain is done, checks `c.Errors` and - if there's
anything there - writes one consistent body:

```go
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) > 0 {
            c.JSON(http.StatusInternalServerError, gin.H{"error": c.Errors.Last().Error()})
        }
    }
}
```

*What just happened:* `c.Next()` runs the rest of the chain (other middleware, then the handler). When it
returns, the handler has finished and may have attached errors. If `c.Errors` is non-empty we write the
single, canonical error shape - `{"error": "..."}` - using the last attached error. Now *every* endpoint
that calls `c.Error` produces identical JSON. Register it once, in front of everything:

```go
r := gin.Default()
r.Use(ErrorHandler())
```

*What just happened:* `r.Use` installs the middleware globally, so it wraps every route. Because it's the
one writing error responses, changing the error format for your whole API is now a one-line edit in one
place - the dream we were chasing.

> 📝 The version above always writes `500`, which is too blunt - a "not found" is a `404`, not a server
> error. The status needs to depend on *what kind* of error it is. That's exactly what the next layer adds.

## 3. Sentinel errors: mapping meaning to status

Right now the middleware can't tell a "not found" from a real crash, because `errors.New("task not
found")` is just a string. The fix is to give errors an *identity* your code can test for. A **sentinel
error** is a package-level error value you compare against:

```go
package store

import "errors"

var (
    ErrNotFound = errors.New("task not found")
    ErrEmptyTitle = errors.New("title must not be empty")
)
```

*What just happened:* these are single, shared values. Your logic returns `ErrNotFound`, and anyone holding
the error can ask "is this *that* error?" with `errors.Is(err, store.ErrNotFound)` - even if the error has
been wrapped on the way up. They're the vocabulary your domain speaks in.

Your logic returns them instead of ad-hoc strings:

```go
func (s *Store) Get(id string) (Task, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    task, ok := s.tasks[id]
    if !ok {
        return Task{}, ErrNotFound
    }
    return task, nil
}
```

*What just happened:* `Get` knows nothing about HTTP status codes - it speaks pure domain. "I couldn't find
it" is `ErrNotFound`, full stop. That's the whole point: this code is testable and reusable without dragging
a `*gin.Context` through it.

Now the handler attaches whatever the store returns, and the middleware does the translation:

```go
func getTask(s *store.Store) gin.HandlerFunc {
    return func(c *gin.Context) {
        task, err := s.Get(c.Param("id"))
        if err != nil {
            c.Error(err)
            return
        }
        c.JSON(http.StatusOK, task)
    }
}
```

```go
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) == 0 {
            return
        }

        err := c.Errors.Last().Err
        switch {
        case errors.Is(err, store.ErrNotFound):
            c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
        case errors.Is(err, store.ErrEmptyTitle):
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        default:
            c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
        }
    }
}
```

*What just happened:* the handler no longer decides status codes at all - it attaches the error and leaves.
The middleware uses `errors.Is` to recognize each sentinel and pick the right HTTP status: `ErrNotFound` →
`404`, `ErrEmptyTitle` → `400`, anything unrecognized → a generic `500` (and notice it does *not* leak the
raw internal error string to the client for the unknown case - you log those, you don't expose them). All
the HTTP knowledge lives here; all the domain knowledge lives in the store. Each layer does one job.

> 💡 This is the payoff of the mental model. "What does this mean?" is decided once, in the store, as a
> typed value. "What HTTP status does that meaning deserve?" is decided once, in the middleware. Handlers
> stop carrying either decision and become the thin translators they were always supposed to be.

## 4. Project structure: splitting the one big file

A single `main.go` was fine when the whole app fit on a screen. With handlers, a store, models, sentinel
errors, and middleware, it's time to give each its own package. The goal isn't ceremony - it's that you
can open the right file by name and that the compiler enforces the layering.

Here's a layout that scales without being heavy:

```
tasks-api/
  go.mod
  main.go              # wire the router + dependencies, then Run
  models/
    task.go            # the Task struct, no logic
  store/
    store.go           # in-memory store + the sentinel errors (business logic + data)
  handlers/
    tasks.go           # HTTP in, HTTP out - thin translators
  middleware/
    errors.go          # the ErrorHandler
```

*What just happened:* the dependency arrow points one way. `handlers` import `store`; `store` imports
`models`; nothing imports `handlers`. `main` is the only place that knows about all of them - it's the
wiring closet. If you ever feel tempted to import `gin` inside `store`, that's the structure telling you a
decision is in the wrong layer.

The piece that ties it together is **dependency injection** - `main` creates the store and *hands* it to
the handlers, rather than the handlers reaching for a global. That's why the handlers in step 3 were
written as `func getTask(s *store.Store) gin.HandlerFunc` - they're closures that capture the store:

```go
// main.go
package main

import (
    "os"

    "github.com/gin-gonic/gin"
    "yourmodule/handlers"
    "yourmodule/middleware"
    "yourmodule/store"
)

func main() {
    s := store.New()                  // create the one shared store

    r := gin.Default()
    r.Use(middleware.ErrorHandler())

    tasks := r.Group("/tasks")
    {
        tasks.GET("", handlers.ListTasks(s))
        tasks.POST("", handlers.CreateTask(s))
        tasks.GET("/:id", handlers.GetTask(s))
        tasks.PUT("/:id", handlers.UpdateTask(s))
        tasks.DELETE("/:id", handlers.DeleteTask(s))
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    r.Run(":" + port)
}
```

*What just happened:* `main` builds the store once and passes `s` into each handler factory, so every
request shares the same data. The handlers never see a global - they only know the store they were handed,
which makes them trivial to test (in Phase 8 you'll hand them a store you control). The route group keeps
the URLs tidy, and the middleware is registered before the routes so it wraps them all.

> 📝 **Config via the environment.** `os.Getenv("PORT")` reads the port from the environment, falling back
> to `8080` for local dev. This is the [twelve-factor](https://12factor.net/config) habit: anything that
> changes between your laptop and production - port, database URL, log level - comes from env vars or
> flags, never hard-coded. Phase 8 leans on this when you containerize and deploy.

That's the whole refactor. The app does exactly what it did at the end of Phase 6, but now a new endpoint
is a small handler in `handlers/`, a method on the store, and one line in `main` - and its errors come out
in the same shape as everything else, for free.

## Recap

- A **handler is a translator**: read HTTP in, hand meaning to your logic, translate the result or error
  back out. Validation and business decisions live *below* it.
- `c.JSON`/`c.AbortWithStatusJSON` write per-handler errors, but you must `return` after writing - 
  `c.JSON` does **not** stop the handler, and falling through writes a second, broken response.
- `c.Error(err)` attaches an error to `c.Errors` without writing anything; an **error middleware** calls
  `c.Next()` then inspects `c.Errors` and writes **one consistent JSON shape** for the whole API.
- **Sentinel errors** (`var ErrNotFound = errors.New(...)`) in your service/store layer give errors an
  identity; the middleware maps them to status codes with `errors.Is`.
- Split the single file into `main` (wiring), `handlers` (HTTP), `store`/`service` (logic + data), and
  `models` - with `main` injecting the store into handlers and config coming from env vars.

## Quick check

```quiz
[
  {
    "q": "After writing an error response with c.JSON inside a handler, why must you call return?",
    "choices": ["c.JSON is asynchronous and return waits for it", "c.JSON does not stop the handler, so without return Go falls through and tries to write a second response", "return is what actually sends the body to the client", "It frees the gin.Context memory"],
    "answer": 1,
    "explain": "c.JSON only writes a response; the handler keeps running. Without return it falls through to later code and writes a second response, causing a 'headers already written' error."
  },
  {
    "q": "What does c.Error(err) do?",
    "choices": ["Immediately writes a 500 JSON response", "Appends the error to c.Errors and writes nothing, leaving the response to a later middleware", "Aborts the request and skips all remaining handlers", "Logs the error to stderr and returns"],
    "answer": 1,
    "explain": "c.Error attaches the error to the context's c.Errors slice without touching the response body. An error-handling middleware inspects c.Errors after c.Next() and writes the response."
  },
  {
    "q": "How does the error middleware turn a store's ErrNotFound into a 404 instead of a 500?",
    "choices": ["By string-matching err.Error() against 'not found'", "By checking the HTTP status the store already set", "By comparing the attached error with errors.Is(err, store.ErrNotFound) and choosing the status", "Gin maps sentinel errors to status codes automatically"],
    "answer": 2,
    "explain": "The store returns the sentinel value ErrNotFound; the middleware uses errors.Is to recognize it (even if wrapped) and picks 404. The HTTP status decision lives in one place."
  }
]
```


---

# Testing & Production

You've built the whole tasks API - routing, binding, middleware, CRUD, error handling, a tidy package layout. Now comes the part that decides whether anyone trusts it: proving it works, and running it somewhere real without it falling over at 3am. Both turn out to be small, once you see the one fact that makes them small.

## The mental model: your router is just an `http.Handler`, so testing is calling it in memory

Here's the thing that makes Gin pleasant to test. A `*gin.Engine` - the thing you get from `gin.Default()` - satisfies Go's `http.Handler` interface. It has a `ServeHTTP(w, r)` method. That's the *exact* same interface the standard library's `http.Server` uses to feed it real requests.

> 💡 If your router is an `http.Handler`, then a test is nothing more than calling `ServeHTTP` yourself with a fake request and a fake response writer. No network. No port. No `go func` running a server in the background. You hand the engine a request, it fills in a response, you read it back - all in memory, in microseconds.

The standard library hands you the two fakes you need in `net/http/httptest`:

- `httptest.NewRequest(method, target, body)` builds a `*http.Request` without a real connection.
- `httptest.NewRecorder()` gives you a `*httptest.ResponseRecorder` - a response writer that records the status, headers, and body into fields you can inspect (`w.Code`, `w.Body`).

Wire those together and you've tested a route end to end without opening a socket.

```go
package main

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
)

func TestListTasks(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := setupRouter()

    w := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
    r.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Fatalf("got status %d, want 200", w.Code)
    }

    var got []Task
    if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
        t.Fatalf("response wasn't valid JSON: %v", err)
    }
}
```

*What just happened:* we built the same router the real app uses (`setupRouter()` - more on that in a second), created a recorder and a GET request for `/api/v1/tasks`, and called `r.ServeHTTP(w, req)`. That single call runs the *entire* chain - middleware, routing, your handler - exactly as a live request would, except nothing left the process. Afterward `w.Code` is the status the handler set and `w.Body` is a `*bytes.Buffer` holding the response body, which we unmarshal to confirm it's the JSON shape we expect. This test runs in well under a millisecond and never touches the network.

Testing a **POST** is the same shape with two additions: you pass a body and set the `Content-Type` header so Gin's `ShouldBindJSON` knows it's JSON.

```go
func TestCreateTask(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := setupRouter()

    body := `{"title":"write tests"}`
    w := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks", bytes.NewBufferString(body))
    req.Header.Set("Content-Type", "application/json")
    r.ServeHTTP(w, req)

    if w.Code != http.StatusCreated {
        t.Fatalf("got status %d, want 201", w.Code)
    }
}
```

*What just happened:* `bytes.NewBufferString(body)` turns our JSON string into an `io.Reader`, which is what the request body wants. Setting `Content-Type: application/json` matters - without it, binding can reject the body or skip it, and you'd be testing the wrong path. We assert a `201 Created`, the status your create handler returns. Same recorder, same `ServeHTTP`, same in-memory speed. (For testing inputs you *expect* to fail - a missing title, a malformed body - send the bad payload and assert the `400` and error message your validation produces.)

This is the heart of testing a web app. The rest - table-driven cases, golden files, running it all in CI on every push - is general Go testing, covered in [testing in CI](/guides/testing-in-ci).

## `gin.TestMode` and the `setupRouter()` you build once

Two small disciplines make the tests above clean.

First, **`gin.SetMode(gin.TestMode)`**. Gin runs in one of three modes - debug (the noisy default), test, and release. Debug mode prints a wall of colored startup output and per-request logging; in a test run that's pure noise drowning your actual failures. `gin.TestMode` silences it. Set it at the top of each test (or once in a `TestMain`).

Second - and this is the structural move that makes everything testable - **factor your router construction into a function**, conventionally `setupRouter()`, that returns the configured `*gin.Engine`. Both `main` and your tests call it, so they exercise the *same* wiring.

```go
func setupRouter() *gin.Engine {
    r := gin.Default()

    v1 := r.Group("/api/v1")
    {
        v1.GET("/tasks", listTasks)
        v1.POST("/tasks", createTask)
        v1.GET("/tasks/:id", getTask)
        v1.PUT("/tasks/:id", updateTask)
        v1.DELETE("/tasks/:id", deleteTask)
    }

    return r
}

func main() {
    r := setupRouter()
    r.Run(":8080")
}
```

*What just happened:* all the route registration lives in one place. `main` builds the router and runs it; a test builds the *same* router and pokes it with `httptest`. There's no second, slightly-different set of routes that "should match production" but quietly drifts - there's one source of truth. The moment you find yourself copy-pasting route setup into a test, stop and pull out a `setupRouter()`. (If your handlers need a database or config, have `setupRouter(deps)` take them as a parameter so tests can pass fakes.)

## Production mode: flip the switch

When you deploy, get Gin out of debug mode. **Release mode** drops the debug logging and the startup warnings, and is the mode Gin expects in production. Two ways to set it:

```go
// In code, before you build the router:
gin.SetMode(gin.ReleaseMode)

// Or via environment variable, no code change:
//   GIN_MODE=release ./your-binary
```

*What just happened:* `gin.ReleaseMode` (or the `GIN_MODE=release` env var, which Gin reads on startup) turns off the per-request debug log lines and the "running in debug mode" warning. The env-var form is usually nicer for deploys - the same binary runs in debug locally and release in production, controlled entirely by the environment. Prefer the env var unless you have a reason to hard-code it.

> 📝 Release mode is about Gin's own chatter, not your application logging. Your `gin.Recovery()` middleware still catches panics, and any logging *you* added still runs. You're silencing the framework's debug noise, not going dark.

## Graceful shutdown: why `r.Run()` isn't enough for a real deploy

You've used `r.Run(":8080")` everywhere, and for learning it's perfect. For a real deploy it has two gaps, and both come from the same root: `r.Run()` is a convenience wrapper that builds an `http.Server` with **default settings** and gives you no handle on it.

The two things you lose:

1. **No timeouts.** A client that opens a connection and sends bytes slowly - or never finishes - can tie up a server goroutine indefinitely. With enough of them (accidental or malicious), you run out of resources. Real servers set read/write/idle timeouts as a baseline defense.
2. **No clean drain.** When your platform restarts the service (a deploy, a scale-down, a `SIGTERM`), `r.Run()` just gets killed mid-flight. Requests in progress are cut off, and clients see broken connections. You want the server to *stop accepting new requests, finish the ones in flight, then exit* - that's a **graceful shutdown**.

You get both by constructing the `http.Server` yourself and handing it your Gin engine as the handler (because - say it with me - the engine is an `http.Handler`):

```go
func main() {
    gin.SetMode(gin.ReleaseMode)
    r := setupRouter()

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      r, // the gin.Engine, used as a plain http.Handler
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Run the server in its own goroutine so main can wait for a shutdown signal.
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server failed: %v", err)
        }
    }()

    // Block until we get an interrupt or terminate signal.
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    <-ctx.Done()
    log.Println("shutting down...")

    // Give in-flight requests up to 5 seconds to finish, then force-close.
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Fatalf("forced shutdown: %v", err)
    }
    log.Println("server stopped cleanly")
}
```

*What just happened:* a lot of small, deliberate pieces. We build an `http.Server` with our engine as `Handler` and real `ReadTimeout`/`WriteTimeout`/`IdleTimeout` values, closing the slow-client gap. We start it with `ListenAndServe()` in a goroutine - note the `err != http.ErrServerClosed` check, because a *clean* shutdown returns that exact error and we don't want to treat it as a crash. Then `signal.NotifyContext` gives us a context that cancels when the OS sends `SIGINT` (Ctrl+C) or `SIGTERM` (what platforms send on restart). `<-ctx.Done()` blocks `main` until that happens. Once it fires, `srv.Shutdown(shutdownCtx)` does the graceful part: it stops accepting new connections and waits for in-flight requests to finish, but only up to the 5-second deadline we set with `context.WithTimeout` - past that, it gives up and forces them closed so a stuck request can't block the deploy forever.

> ⚠️ The `http.ErrServerClosed` check is not optional decoration. `Shutdown` causes `ListenAndServe` to return `http.ErrServerClosed`. If your goroutine does a blanket `log.Fatalf` on *any* error, every clean shutdown will look like a crash and exit non-zero - which your orchestrator may then report as a failed restart. Treat that one error as success.

This is more code than `r.Run(":8080")`, and that's the point: you've traded one line for control over timeouts and a clean exit, which is exactly the trade a production service needs to make.

## Deploy shape: a static binary, env config, a small container

Go's superpower for shipping is the **static binary**. A Go program compiles to a single executable with no runtime to install - no interpreter, no `node_modules`, no virtualenv. For a Linux container, build it fully static:

```bash
CGO_ENABLED=0 GOOS=linux go build -o tasks-api .
```

*What just happened:* `CGO_ENABLED=0` disables cgo so the binary doesn't dynamically link against the system C library - it's fully self-contained and will run on a bare `scratch` or `alpine` image with nothing else installed. `GOOS=linux` cross-compiles for Linux even if you're building on a Mac or Windows machine. The output is one file, `tasks-api`, that you can copy somewhere and run.

Read configuration - at minimum the **port** - from the environment, not a hard-coded constant. Most platforms (and the 12-factor convention) tell your app which port to bind via a `PORT` env var:

```go
addr := ":8080"
if p := os.Getenv("PORT"); p != "" {
    addr = ":" + p
}
srv := &http.Server{Addr: addr, Handler: r /* ...timeouts... */}
```

*What just happened:* we default to `:8080` for local dev but let `PORT` override it, so the same binary runs unchanged whether you run it on your laptop or a platform that injects `PORT=10000`. Same principle applies to database URLs, secrets, and the `GIN_MODE` we set earlier - configuration comes from the environment so the artifact stays identical across environments.

A minimal multi-stage Dockerfile builds the binary and copies *only* it into a tiny final image:

```bash
# Build stage
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /tasks-api .

# Run stage - tiny image, just the binary
FROM gcr.io/distroless/static
COPY --from=build /tasks-api /tasks-api
EXPOSE 8080
ENV GIN_MODE=release
ENTRYPOINT ["/tasks-api"]
```

*What just happened:* the first stage has the whole Go toolchain and compiles the binary; the second stage is a `distroless/static` image - basically nothing but the files needed to run a static binary, no shell, no package manager - and we copy just the one executable into it. The result is a container measured in single-digit megabytes instead of hundreds, with a tiny attack surface. We also bake in `GIN_MODE=release` so the container always runs in production mode.

In front of it, put a **reverse proxy** - nginx, Caddy, or whatever your platform provides (a load balancer, an ingress controller). The proxy terminates TLS (HTTPS), can serve static assets, and load-balances across multiple instances of your binary. Your Gin app speaks plain HTTP on its port; the proxy handles the public-facing internet. You generally don't terminate TLS in Gin itself - let the proxy do it.

That's the whole deploy shape: one static binary, configured by env vars, in a small container, behind a proxy. Taking it the rest of the way to a live URL - picking a host, wiring CI, the domain and TLS specifics - is covered in [ship your side project](/guides/ship-your-side-project).

## Recap

- A `*gin.Engine` is an `http.Handler`, so you **test it in memory** with `net/http/httptest`: build a request with `httptest.NewRequest`, a recorder with `httptest.NewRecorder`, call `r.ServeHTTP(w, req)`, then inspect `w.Code` and `w.Body`. No network, no ports. For POST, pass a `bytes.NewBufferString` body and set `Content-Type: application/json`.
- Call `gin.SetMode(gin.TestMode)` in tests to silence debug output, and factor route construction into a `setupRouter()` that both `main` and tests call - one source of truth, no drift.
- For production, switch to **release mode** via `gin.SetMode(gin.ReleaseMode)` or `GIN_MODE=release` (the env var is nicer for deploys).
- `r.Run()` gives you no timeouts and no clean drain. Build your own `http.Server{Addr, Handler: r, ReadTimeout, WriteTimeout, IdleTimeout}`, run it in a goroutine, and on `SIGINT`/`SIGTERM` call `srv.Shutdown(ctx)` for a **graceful shutdown** - treating `http.ErrServerClosed` as success.
- Ship a **static binary** (`CGO_ENABLED=0 go build`) in a small container, read config like `PORT` from the environment, and put a reverse proxy (nginx/Caddy/platform LB) in front to terminate TLS and load-balance.

## Quick check

Lock in the core fact (the handler interface) and the two production must-haves:

```quiz
[
  {
    "q": "Why can you test a Gin router with net/http/httptest and no real network?",
    "choices": ["Gin spins up a hidden test server on a random port", "A *gin.Engine implements http.Handler, so a test just calls its ServeHTTP method directly in memory", "httptest mocks the TCP stack at the OS level", "You can't - Gin tests always need a running server"],
    "answer": 1,
    "explain": "Because the engine satisfies http.Handler, ServeHTTP(w, req) runs the entire middleware-and-handler chain in-process. httptest gives you a fake request and a recording response writer; nothing touches a socket."
  },
  {
    "q": "What does r.Run(\":8080\") NOT give you that a real deploy needs?",
    "choices": ["Routing and middleware", "JSON responses", "Configurable read/write timeouts and a graceful shutdown", "The ability to set a port"],
    "answer": 2,
    "explain": "r.Run is a convenience wrapper over a default http.Server with no exposed handle. To set ReadTimeout/WriteTimeout/IdleTimeout and to drain in-flight requests on SIGTERM via srv.Shutdown(ctx), construct the http.Server yourself with your engine as the Handler."
  },
  {
    "q": "During a graceful shutdown, srv.ListenAndServe() returns a specific error. How should you treat it?",
    "choices": ["As a fatal crash - log.Fatal and exit non-zero", "As http.ErrServerClosed, which signals a clean shutdown and should NOT be treated as a failure", "Ignore the return value of ListenAndServe entirely", "Retry ListenAndServe in a loop"],
    "answer": 1,
    "explain": "srv.Shutdown causes ListenAndServe to return http.ErrServerClosed. Check for it explicitly; a blanket log.Fatal on any error would make every clean shutdown look like a crash."
  }
]
```


---

# Where to Go Next

Stop and look at what you can actually do now. You can spin up a Gin server, route requests with path and query parameters, group routes, bind and validate JSON into structs, shape responses with `c.JSON` and the right status codes, write and chain middleware around `c.Next()`, build full CRUD for a resource, handle errors with `c.Error` and `AbortWithStatusJSON`, structure the project past one file, and test the whole thing with `httptest` before shipping it with graceful shutdown. That's a real REST API, not a toy.

And here's the quieter win. Because Gin is so small, you didn't only learn a framework - you saw what one *is*. An **engine** holds the routes, a **context** carries each request, and middleware wraps the chain. Everything else is helpers over the standard library's `net/http`. Nothing was hidden behind magic, which means when something breaks at 2am, you can actually reason about it.

This last phase isn't more handlers - it's the map: where Gin sits among the other Go web frameworks, the layer you'll almost certainly add next, and one concrete thing to go build.

## Gin vs the field

You now know enough to choose a framework *on purpose* rather than by reputation. The good news in Go: these frameworks are far more alike than the JavaScript world's are. They all sit on (or near) `net/http`, they all do routing, params, and middleware. The differences are about *feel* and *tradeoffs*, not whole different universes.

```mermaid
flowchart TD
  Start[Need a Go web service?] --> Std{Want stdlib purity?}
  Std -- Yes, minimal --> Chi[chi or net/http]
  Std -- No, want batteries --> Style{Handler style?}
  Style -- Context helpers --> Gin[Gin]
  Style -- Return an error --> Echo[Echo]
  Style -- Express-like, max speed --> Fiber[Fiber]
```

A line on each:

- **Gin** - the most popular, the biggest ecosystem, the most Stack Overflow answers. Handlers take a `*gin.Context` and write to it. This is the safe default and the one you'll meet most in Go jobs. (You're here.)
- **Echo** - very close to Gin in spirit, with one stylistic difference worth knowing: its handlers *return* an `error` (`func(c echo.Context) error`) instead of writing failures into the context. It also ships a bit more built-in middleware. If you prefer the error-returning style, you'll like it. See [Echo From Zero](/guides/echo-from-zero).
- **chi** - minimal and proudly so. It's a router that stays *pure* `net/http` - handlers are plain `http.HandlerFunc`, middleware is standard `func(http.Handler) http.Handler`. Nothing to unlearn, nothing locked in. See [chi From Zero](/guides/chi-from-zero).
- **Fiber** - an Express-like API that feels familiar if you came from Node. The real tradeoff: it's built on **fasthttp**, not `net/http`, so it can be faster but is **not** compatible with the standard library's ecosystem of handlers and middleware. That's a genuine fork in the road, not a free lunch.
- **The standard library alone** - for many services, `net/http` plus the routing improvements in modern Go is genuinely enough. Knowing what Gin saves you starts with knowing what you'd write by hand. See [Web Services With Only net/http](/guides/web-services-with-only-net-http).

> 💡 How to pick: reach for **Gin** when you want batteries plus the biggest ecosystem. Reach for **chi or net/http** when you want stdlib purity and zero lock-in. Reach for **Echo** if you prefer its error-returning handler style. Reach for **Fiber** only if its API and raw speed genuinely win for you - and you've accepted that you're stepping off the `net/http` standard.

📝 None of these is "the best." They're aimed at slightly different tastes. The senior instinct isn't memorizing a winner - it's asking "best for *this* job?" and being able to answer straight. You have the pieces for that now.

## The layer you'll add next: a real database

Every API in this guide stored tasks in memory. That's perfect for learning and useless in production - restart the server and the data's gone. The very next thing almost every real Gin service grows is a **database**.

Here's the reassuring part: your handlers barely change. Remember how Phase 6 and 7 kept the HTTP logic separate from where the data lived? That paid off. The handler still binds JSON, validates, calls a store, and returns a response. All that swaps underneath is the store - from a map to a database-backed one.

[GORM From Zero](/guides/gorm-from-zero) is the natural next read. GORM is Go's most popular ORM: you define your `Task` struct, point it at SQLite (or Postgres later), and your create/read/update/delete calls become real persistence. The shape of your code from Phases 6 and 7 stays intact - you're replacing the bottom layer, not rewriting the top.

## What to build

Reading more won't make this stick. Building one real thing will. So here's the assignment, and it's deliberately concrete.

Take the **tasks API** you grew across this guide and carry it all the way home:

- **Swap the in-memory store for GORM + SQLite** so tasks survive a restart. The handlers stay; the store changes. ([GORM From Zero](/guides/gorm-from-zero) walks the persistence part.)
- **Add JWT auth middleware** so each request proves who it is, and tasks belong to a user. This is exactly the middleware pattern from Phase 5, applied to a real job.
- **Add request logging** (and, when you're ready, basic metrics) so you can see what your service is doing in production.
- **Generate API docs** with OpenAPI/Swagger via **swaggo**, so other people - and future you - can read the contract.
- **Tidy up config** so secrets and ports come from the environment, not hardcoded values.
- **Deploy it** somewhere you can hit from your phone, with graceful shutdown wired up the way Phase 8 showed.

If the tasks API feels too familiar, build something small and new end to end instead - a **URL shortener** or a **notes API**. Same muscles: routes, binding, a store, middleware, tests, deploy. The point is finishing one project completely, which teaches more than three more tutorials would.

## The clear-eyed close

Gin was never magic. Strip the helpers away and it's three things you now understand completely: an **engine** that holds your routes, a **context** that carries each request, and a **middleware chain** that wraps the whole thing - all sitting on the same `net/http` you could write by hand if you had to.

That's why you can read the machine now. You can build a real service on top of Gin, and - more importantly - you can reason about it when it misbehaves. Go finish the tasks API, give it a database, lock it behind auth, deploy it, and show someone. You're ready.

## Recap

1. **You can ship a real Gin API** - routed, bound and validated, middleware-wrapped, structured, tested, and deployed - and you understand *why* each piece works, because Gin hid nothing.
2. **Choose a framework on purpose** - Gin for batteries + ecosystem, chi/net-http for stdlib purity and zero lock-in, Echo for its error-returning handlers, Fiber only when its API/speed wins and you accept the non-`net/http` base.
3. **A database is the next layer** - most Gin services add one, and with the Phase 6/7 separation in place your handlers barely change; you swap the in-memory store for GORM.
4. **Build and finish one thing** - carry the tasks API to GORM + SQLite, JWT auth, request logging, OpenAPI docs, real config, and a deploy. Or build a small URL shortener / notes API end to end.
5. **Gin is small on purpose** - an engine, a context, and a middleware chain over the standard library you now understand. That smallness was the lesson, not a limit.

## Quick check

Three decisions to take with you as you leave this guide:

```quiz
[
  {
    "q": "You want the biggest ecosystem and the most community answers, and you're happy writing handlers that take a context object. Which framework is the on-purpose default?",
    "choices": [
      "Fiber, because it's the fastest",
      "Gin, the most popular Go web framework with context-style handlers",
      "chi, because it's minimal",
      "net/http alone, always"
    ],
    "answer": 1,
    "explain": "Gin is the most popular Go web framework, with the largest ecosystem and context-style handlers. chi and net/http favor stdlib purity; Echo prefers error-returning handlers; Fiber trades net/http compatibility for speed."
  },
  {
    "q": "Which statement about Fiber is the real tradeoff?",
    "choices": [
      "Fiber is just Gin with a different name",
      "Fiber is built on fasthttp, not net/http, so it can be faster but is not compatible with the standard library's handlers and middleware",
      "Fiber is the only Go framework with middleware",
      "Fiber cannot handle JSON"
    ],
    "answer": 1,
    "explain": "Fiber's Express-like API sits on fasthttp rather than net/http. That can mean more speed, but it steps off the standard library, so net/http-compatible handlers and middleware don't carry over. That's a real tradeoff to accept on purpose."
  },
  {
    "q": "You're adding a real database to your tasks API from Phases 6 and 7. What mostly changes?",
    "choices": [
      "Every handler must be rewritten from scratch",
      "Only the store layer swaps from an in-memory map to a GORM-backed one; the handlers stay roughly the same",
      "You must abandon Gin and switch to Echo",
      "Nothing - Gin stores data in a database automatically"
    ],
    "answer": 1,
    "explain": "Because the HTTP logic was kept separate from where data lives, the handlers still bind, validate, call a store, and respond. You swap the store from a map to GORM + a database - the bottom layer changes, the top stays."
  }
]
```
