# Echo From Zero

> Learn Echo, the high-performance Go web framework: the instance and your first server, routing and groups, binding and validation, responses and rendering, middleware, a full REST API with centralized error handling, and testing and production. A clean, batteries-included alternative to Gin you'll find across Go shops.


---

# Echo From Zero

Echo is the other name you'll hear constantly in Go web work. Like [Gin](/guides/gin-from-zero), it's a
fast, focused framework over `net/http` - but with a slightly cleaner handler signature, a first-class
error-return style, and a generous set of built-in middleware. If Gin feels like "net/http with helpers,"
Echo feels like "net/http with helpers *and* good manners about errors." Many teams pick it precisely for
that: handlers that *return* an error instead of writing one by hand, and a centralized handler that turns
those errors into HTTP responses.

The mental model is one instance and one context. The **instance** (`echo.Echo`, made with `echo.New()`)
is your application - you register routes and middleware on it and start it. Every request gets an
**`echo.Context`**, the one value that reads input and writes output. The Echo twist worth holding onto:
a handler is `func(c echo.Context) error` - you **return** errors, and Echo's error handler decides what
the client sees. That single design choice shapes how clean Echo apps stay.

> 📝 This teaches the **framework** - it assumes you know **Go** ([Go From Zero](/guides/go-from-zero)).
> It's most illuminating read alongside [Gin](/guides/gin-from-zero) (the closest comparison) and
> [chi](/guides/chi-from-zero) (the minimalist), with the [net/http roots guide](/guides/web-services-with-only-net-http)
> showing the foundation under all three. Echo runs as a Go program, so examples are shown with the
> commands to run them.

## How to read this

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

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What Echo Is & Your First Server](01-what-echo-is.md)** 🟢 - the instance, the context, the error-returning handler, and a running server.
2. **[Routing & Groups](02-routing-and-groups.md)** 🟢 - methods, path and query params, and route groups.
3. **[Binding & Validation](03-binding-and-validation.md)** 🟡 - `c.Bind`, struct tags, and plugging in a validator.

**Part 2 - A real API (🟡 → 🔴)**
4. **[Responses & Rendering](04-responses-and-rendering.md)** 🟡 - `c.JSON`, status codes, templates, and static files.
5. **[Middleware](05-middleware.md)** 🟡 - the middleware signature, built-ins (Logger/Recover/CORS), and writing your own.
6. **[A REST API with Error Handling](06-rest-api-and-errors.md)** 🔴 - full CRUD plus Echo's centralized `HTTPErrorHandler`.

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

> The throughline: an **instance** holds your routes, a **context** handles each request, and handlers
> **return errors** for a central handler to render. That error style is Echo's whole personality.


---

# What Echo Is & Your First Server

You already know [Go](/guides/go-from-zero), and you've maybe met [Gin](/guides/gin-from-zero) - the
most popular Go web framework. Echo is its closest peer: another fast, focused layer over the standard
library's `net/http`. Echo, like Gin, handles the repetitive parts - routing, JSON, middleware - without
hiding what's underneath.

So why pick Echo over Gin? One design choice, mostly: in Echo, **a handler returns an error**. Not
`func(c *gin.Context)` with no return value, where you write the error response by hand - but
`func(c echo.Context) error`, where you hand the error back and a central handler turns it into an HTTP
response. In a real codebase with dozens of endpoints, that's the difference between every handler
re-implementing "how do I report a failure" and every handler saying `return err` - less boilerplate, and
errors that come out consistent because one place renders them all.

💡 **Echo is net/http with helpers *and* opinions about errors.** The helpers make it fast to write; the
error-return style makes it stay clean as it grows. That's the whole pitch.

## The mental model: instance holds routes, context handles the request, handlers return errors

Before any code, hold three things in your head. They are the entire framework.

📝 **The instance** (`*echo.Echo`, made with `echo.New()`) is your application. You create it once,
register all your routes and middleware on it, and start it.

📝 **The context** (`echo.Context`) is one value handed to you for *each incoming request*. It carries
the request, the response writer, the path and query params, and every helper you use to read input and
write output. Note that `echo.Context` is an *interface*, not a struct - good to know, won't matter today.

📝 **The handler returns an error.** Every handler you write has the shape `func(c echo.Context) error`.
You do your work, then `return c.JSON(...)` on success or `return someError` on failure. Echo's central
error handler decides what the client actually sees.

Say it once: **the instance holds the routes, the context handles the request, and the handler returns
an error.** Everything else in Echo is detail.

```mermaid
flowchart LR
  E[echo.Echo<br/>holds the routes] --> R[route<br/>GET /ping]
  R --> H["handler<br/>func(c echo.Context) error"]
  H --> C[echo.Context<br/>reads input, writes output]
  H -.returns error.-> X[central error handler<br/>renders the response]
```

*One idea:* the instance matches an incoming request to a route, calls that route's handler, and the
handler uses the context to send a response - or returns an error for the central handler to render.
Every Echo endpoint flows along those arrows.

## Your first server

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

```bash
go get github.com/labstack/echo/v4
```

*What just happened:* `go get` downloaded Echo (the `/v4` is the current major version) and added it to
your `go.mod`/`go.sum`. The import path is `github.com/labstack/echo/v4`; you refer to it in code as the
`echo` package.

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

```go
package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    e.GET("/ping", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
    })
    e.Logger.Fatal(e.Start(":1323"))
}
```

*What just happened:* line by line - 
- `echo.New()` creates the **instance** and returns a `*echo.Echo`. We name it `e`.
- `e.GET("/ping", ...)` registers a **route**: when a `GET` request arrives for `/ping`, run the
  function we pass. That function is the **handler**, and its signature - `func(c echo.Context) error` - 
  is the shape every Echo handler has.
- Inside the handler, `c.JSON(http.StatusOK, ...)` uses the **context** to write the response: sets the
  status to `200`, sets `Content-Type` to `application/json`, serializes the value, and sends it.
  `c.JSON` *returns an error*, and we `return` it - so if writing the response fails, Echo knows. On the
  happy path it returns `nil`, which Echo reads as "all good."
- `http.StatusOK` is the standard library's name for `200`. Echo leans on `net/http`'s constants rather
  than inventing its own.
- `e.Start(":1323")` starts the server listening on port 1323 (Echo's docs use that port; nothing magic
  about it). It blocks until you stop the program. We wrap it in `e.Logger.Fatal(...)` so that if `Start`
  returns an error - say the port is already taken - it's logged and the program exits.

Run it like any Go program:

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

```console
$ go run main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/  v4
⇨ http server started on [::]:1323
```

*What just happened:* `go run` compiled and started your program, and `e.Start` brought up the server.
Leave it running and, in another terminal, hit the route:

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

*What just happened:* `curl` sent a `GET /ping`. The instance matched it to your route, called your
handler, and the handler used the context to write back JSON - returning `nil` to signal success.

## The error-returning handler, and why it's different

This is the one place Echo and Gin diverge in a way worth pausing on. Put the two handler shapes side by
side:

```go
// Gin: no return value - you write the response (and any error) yourself.
func(c *gin.Context) {
    c.JSON(200, gin.H{"message": "pong"})
}

// Echo: return an error - success or failure flows back to a central handler.
func(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
}
```

*What just happened:* both write the same JSON. But the Echo version *returns* - and that return value is
the hook. When something goes wrong, you don't write a status code and an error body by hand every time.
You return an error, and Echo's central error handler renders it. Echo even gives you a purpose-built
error type for this:

```go
e.GET("/secret", func(c echo.Context) error {
    return echo.NewHTTPError(http.StatusUnauthorized, "you shall not pass")
})
```

*What just happened:* instead of manually setting a `401` status and writing a JSON body, you returned
an `*echo.HTTPError` describing the failure. Echo's default error handler turns it into a clean `401`
response with a JSON message - every endpoint gets the same consistent shape, no copy-pasted error code.

⚠️ Don't worry about *configuring* that central handler yet - that's Phase 6's job, where we wire up a
custom `HTTPErrorHandler` for the books API. For now, just internalize the habit: **in Echo, you
`return` your result, success or failure.** Forgetting the `return` is the rookie Echo bug - the handler
compiles, but nothing gets sent and you stare at a hung request wondering why.

## Adding Logger and Recover middleware

Here's a sharp difference from Gin worth knowing on day one. Gin's `gin.Default()` hands you a Logger and
a Recovery handler already wired up. **Echo's `echo.New()` does not** - it gives you a bare instance.
Logging and panic-recovery are opt-in. Most apps want both, so you add them yourself:

```go
package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()

    e.Use(middleware.Logger())  // a tidy log line per request
    e.Use(middleware.Recover()) // catch panics, return 500, stay alive

    e.GET("/ping", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
    })

    e.Logger.Fatal(e.Start(":1323"))
}
```

*What just happened:* `e.Use(...)` registers **middleware** - code that runs around every request. We
imported `github.com/labstack/echo/v4/middleware` (a separate package from `echo`) and added two pieces:
- **`middleware.Logger()`** prints a line for every request - method, path, status, how long it took.
- **`middleware.Recover()`** catches a panic inside any handler, turns it into a clean `500` response,
  and keeps the server running. Without it, one panicking handler takes down the whole process.

We'll cover the middleware signature and write our own in Phase 5. ⚠️ Unlike Gin, Echo doesn't include
these by default - if your server runs silent or dies on a panic, it's because you haven't added them yet.

## The running example: a books API

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

```go
type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
}
```

*What just happened:* we declared the `Book` struct the whole guide builds on. Those `json:"..."`
**struct tags** tell Echo what to call each field in JSON - so `Title` becomes `"title"`, not `"Title"`.
Tags work both directions; binding incoming JSON in Phase 3 leans on them too. Here's the type returning
itself through the now-familiar flow:

```go
e.GET("/books/sample", func(c echo.Context) error {
    b := Book{ID: 1, Title: "The Go Programming Language", Author: "Donovan & Kernighan"}
    return c.JSON(http.StatusOK, b)
})
```

*What just happened:* the handler built a `Book`, and `return c.JSON(...)` serialized it using those
tags - no map needed when you already have a struct.

```console
$ curl localhost:1323/books/sample
{"id":1,"title":"The Go Programming Language","author":"Donovan & Kernighan"}
```

By the end of the guide this grows into full create/read/update/delete over a real collection of books,
with centralized error handling and tests. For now you've met the cast: an **instance**, a **route**, a
**handler that returns an error**, a **context**, and the **`Book`** we'll turn into a proper REST API.
Next up: routing - path params, query params, and grouping routes so they don't sprawl.

## Recap

- **Echo is a fast Go web framework over `net/http`** - a close peer of Gin. It does the repetitive
  parts (routing, JSON, middleware) without hiding the standard library underneath.
- **The mental model is three things:** the **instance** (`*echo.Echo`, from `echo.New()`) holds your
  routes; the **context** (`echo.Context`, an interface) handles each request; the **handler returns an
  error** - `func(c echo.Context) error`.
- **Echo's signature trait is the error-returning handler.** You `return c.JSON(...)` on success or
  `return echo.NewHTTPError(...)` on failure, and a central handler renders it. ⚠️ Forgetting the
  `return` is the classic Echo bug.
- **A first server is tiny:** `echo.New()` makes the instance, `e.GET(path, handler)` registers a route,
  `c.JSON(http.StatusOK, ...)` writes the response, and `e.Start(":1323")` listens. Run with
  `go run main.go`, test with `curl`.
- **Unlike Gin, Echo includes no middleware by default.** Add `middleware.Logger()` and
  `middleware.Recover()` yourself (from `github.com/labstack/echo/v4/middleware`) for request logging
  and crash protection.
- **The throughline:** instance → route → handler → context → response, with errors flowing to a central
  handler. We'll grow one **books API** along that path for the rest of the guide.

## Quick check

Three questions on the ideas that have to stick - what Echo is, the instance/context split, and the
error-returning handler:

```quiz
[
  {
    "q": "What is Echo's signature difference from Gin in how handlers work?",
    "choices": [
      "An Echo handler returns an error (func(c echo.Context) error), and a central handler renders it; a Gin handler returns nothing and writes errors by hand",
      "Echo handlers take no arguments at all",
      "Echo handlers must return a string that becomes the response body",
      "Echo handlers run in a separate goroutine automatically"
    ],
    "answer": 0,
    "explain": "Echo handlers are func(c echo.Context) error. You return c.JSON(...) on success or return an error on failure, and Echo's central error handler turns errors into responses. Gin's func(c *gin.Context) has no return value, so you write error responses yourself."
  },
  {
    "q": "What does echo.New() give you that gin.Default() includes but echo.New() does not?",
    "choices": [
      "Nothing extra - echo.New() returns a bare instance, so you add Logger and Recover middleware yourself with e.Use(...)",
      "A built-in database connection",
      "Automatic HTTPS certificates",
      "Logger and Recover middleware, already wired up like gin.Default()"
    ],
    "answer": 0,
    "explain": "echo.New() returns a bare *echo.Echo with no middleware. Unlike gin.Default() (which ships Logger + Recovery), in Echo you opt in: e.Use(middleware.Logger()) and e.Use(middleware.Recover()) from github.com/labstack/echo/v4/middleware."
  },
  {
    "q": "In the mental model, what are the roles of the instance and the context?",
    "choices": [
      "The instance (*echo.Echo) holds your routes and is started once; the context (echo.Context) is handed to a handler for each request to read input and write output",
      "They are the same object with two names",
      "The context holds the routes and the instance handles each request",
      "The instance is the JSON serializer and the context is the router"
    ],
    "answer": 0,
    "explain": "Instance holds the routes, context handles the request. You create one *echo.Echo, register routes on it, and start it; each incoming request gets an echo.Context carrying the request, response writer, params, and helpers. Every handler is func(c echo.Context) error."
  }
]
```


---

# Routing & Groups

In Phase 1 you stood up one route and watched Echo answer it. Real APIs have many routes, and the
first thing that bites people is *which* route answered *which* request.

## A route is method + path → handler

📝 A route in Echo is three things glued together: an **HTTP method** (`GET`, `POST`, …), a **path**
(`/books`, `/books/:id`), and a **handler** (`func(c echo.Context) error`). When a request arrives, Echo
looks at the method *and* the path, finds the one handler registered for that pair, and calls it. Nothing
more mysterious than that.

That "method *and* path" part matters. `GET /books` and `POST /books` are two different routes with two
different handlers, even though the path text is identical. People coming from frameworks that only key on
the path get tripped up here - Echo treats the verb as part of the address.

Under the hood Echo stores all your routes in a **radix tree** (a prefix tree). You never touch it, but
it's why matching stays fast even with hundreds of routes, and why a literal path like `/books/new` can
coexist with a parameter path like `/books/:id` without a linear scan.

A **group** is the second idea: a set of routes that share a common path prefix (and, later, shared
middleware). `/api/v1/books` and `/api/v1/authors` clearly belong together; a group lets you say
"`/api/v1`" once instead of typing it on every route.

We'll grow the **books API** from Phase 1 - the same `Book{id, title, author}` shape - into a small set
of real routes.

## Registering methods

Echo gives you one function per HTTP method, each with the same `(path, handler)` signature:

```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

// Our "database" for now: a slice in memory.
var books = []Book{
	{ID: 1, Title: "The Go Programming Language", Author: "Donovan & Kernighan"},
	{ID: 2, Title: "Go in Action", Author: "Kennedy"},
}

func main() {
	e := echo.New()

	e.GET("/books", listBooks)    // read the collection
	e.POST("/books", createBook)  // add to the collection

	e.Logger.Fatal(e.Start(":1323"))
}

func listBooks(c echo.Context) error {
	return c.JSON(http.StatusOK, books)
}

func createBook(c echo.Context) error {
	return c.JSON(http.StatusCreated, Book{ID: 3, Title: "TODO", Author: "TODO"})
}
```

*What just happened:* `e.GET` and `e.POST` each registered a route on the **same path** `/books` but for
different verbs, pointing at different handlers. A `GET /books` request runs `listBooks`; a `POST /books`
runs `createBook`. (`createBook`'s response is hard-coded for now - reading the request body is Phase 3.)

The full set is `e.GET`, `e.POST`, `e.PUT`, `e.PATCH`, `e.DELETE`, `e.HEAD`, and `e.OPTIONS` - all
`(path, handler)`. There's also `e.Any(path, handler)`, which registers the handler for *every* method at
that path. Reach for `e.Any` rarely; being explicit about verbs is usually clearer and safer.

## Path params: capturing pieces of the URL

You don't want a separate route per book ID. Instead you declare a **path parameter** with a `:name`
segment, and read it back inside the handler with `c.Param("name")`.

```go
func main() {
	e := echo.New()

	e.GET("/books", listBooks)
	e.GET("/books/:id", getBook) // :id is a path parameter

	e.Logger.Fatal(e.Start(":1323"))
}

func getBook(c echo.Context) error {
	id := c.Param("id") // always a string, e.g. "2" from /books/2

	for _, b := range books {
		// strconv.Itoa converts the int ID to a string to compare.
		if id == strconv.Itoa(b.ID) {
			return c.JSON(http.StatusOK, b)
		}
	}

	return c.JSON(http.StatusNotFound, map[string]string{"error": "book not found"})
}
```

*What just happened:* the route `/books/:id` matches any single segment after `/books/` and stashes it
under the name `id`. A request to `/books/2` makes `c.Param("id")` return the string `"2"`. ⚠️ Path
params are **always strings** - convert them yourself (here with `strconv.Itoa`; you'll more often parse
with `strconv.Atoi`). Remember to add `"strconv"` to your imports.

There's also a **wildcard** segment, `*`, for "match the rest of the path, slashes and all." It's mostly
used for serving files:

```go
e.GET("/files/*", func(c echo.Context) error {
	path := c.Param("*") // e.g. "docs/intro.pdf" for /files/docs/intro.pdf
	return c.String(http.StatusOK, "you asked for: "+path)
})
```

*What just happened:* unlike `:id`, which captures exactly one segment, `*` captures everything after
`/files/` including any `/`, read with `c.Param("*")`. Use it sparingly - static assets or catch-alls,
not normal API routes, where named params read better.

## Query params: the bit after the `?`

Path params identify *which* resource. **Query params** - the `?key=value` pairs at the end of a URL - 
usually *filter* or *modify* a request. Think `GET /books?author=Kennedy`. They're optional by nature, so
Echo reads them differently: `c.QueryParam("name")` returns the value, or an empty string `""` if it
wasn't supplied.

```go
func listBooks(c echo.Context) error {
	author := c.QueryParam("author") // "" if ?author= was not in the URL

	if author == "" {
		return c.JSON(http.StatusOK, books) // no filter: return all
	}

	var filtered []Book
	for _, b := range books {
		if b.Author == author {
			filtered = append(filtered, b)
		}
	}
	return c.JSON(http.StatusOK, filtered)
}
```

*What just happened:* `GET /books` returns everything, while `GET /books?author=Kennedy` returns only the
matching ones. `c.QueryParam("author")` never errors on a missing param - it just hands back `""`. ⚠️
That's a footgun if you treat `""` as "no books matched" instead of "no filter requested," so we check
for the empty string *first*.

Need everything at once? `c.QueryParams()` returns a `url.Values` (a `map[string][]string`) holding every
query key and its value(s):

```go
func searchBooks(c echo.Context) error {
	params := c.QueryParams() // url.Values, e.g. {"author": ["Kennedy"], "sort": ["title"]}
	return c.JSON(http.StatusOK, params)
}
```

*What just happened:* `c.QueryParams()` gives you the whole bag of query values, handy when a single key
can repeat (`?tag=go&tag=web`) or you want to loop over unknown filters. For one known key, stick with
`c.QueryParam` - it's simpler.

## Groups: say the prefix once

As the API grows you'll want a version prefix like `/api/v1` so you can ship `/api/v2` later without
breaking existing clients. Typing `/api/v1/...` on every route is tedious and easy to get wrong. A
**group** fixes that.

```go
func main() {
	e := echo.New()

	v1 := e.Group("/api/v1") // every route below is prefixed with /api/v1

	v1.GET("/books", listBooks)        // -> GET /api/v1/books
	v1.GET("/books/:id", getBook)      // -> GET /api/v1/books/:id
	v1.POST("/books", createBook)      // -> POST /api/v1/books

	e.Logger.Fatal(e.Start(":1323"))
}
```

*What just happened:* `e.Group("/api/v1")` returns a group value (`v1`) that carries the prefix. Calling
`v1.GET("/books", ...)` registers the route at the **combined** path `/api/v1/books`. The group has the
same method functions as the instance - `v1.GET`, `v1.POST`, and so on - so routes read cleanly with the
shared prefix factored out.

💡 The bigger payoff is middleware. A group can attach middleware that runs only for its routes - for
example, requiring auth on everything under `/admin`:

```go
// authMiddleware is defined in Phase 5 - shown here only to make the shape concrete.
admin := e.Group("/admin", authMiddleware)   // middleware as a second argument
admin.GET("/stats", adminStats)              // protected: auth runs first

// You can also attach it after creating the group:
admin.Use(authMiddleware)
```

*What just happened:* passing `authMiddleware` as the second argument to `e.Group` (or calling
`admin.Use(...)`) means every route in that group runs the middleware before its handler - so `/admin/*`
is protected without repeating the check in each handler. **What middleware actually is, and how to
write `authMiddleware`, is Phase 5.** For now, hold the shape: groups bundle a prefix *and* shared middleware.

## Recap

- A route is **method + path → handler**; `GET /books` and `POST /books` are distinct routes that share a
  path but not a handler.
- Register routes with `e.GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS(path, handler)`, or `e.Any` for every
  method at once.
- **Path params** (`/books/:id`) are read with `c.Param("id")` and are **always strings** - convert them
  yourself. The wildcard `*` captures the rest of the path via `c.Param("*")`.
- **Query params** are read with `c.QueryParam("author")` (returns `""` when absent) or
  `c.QueryParams()` for the whole `url.Values` bag.
- **Groups** (`e.Group("/api/v1")`) factor out a shared prefix, and can carry shared middleware via a
  second argument or `g.Use(...)`.

## Quick check

```quiz
[
  {
    "q": "You register e.GET(\"/books\", listBooks). A request comes in as POST /books. What happens?",
    "choices": ["listBooks runs anyway", "Echo returns 405 Method Not Allowed because no handler matches that method+path", "Echo runs the first route in the tree", "The server panics"],
    "answer": 1,
    "explain": "A route is method AND path. GET /books and POST /books are different routes; with no POST handler registered, Echo responds 405 Method Not Allowed."
  },
  {
    "q": "For the route /books/:id, what does c.Param(\"id\") return for a request to /books/42?",
    "choices": ["The integer 42", "The string \"42\"", "nil", "An error you must handle"],
    "answer": 1,
    "explain": "Path params are always strings. c.Param(\"id\") returns \"42\"; convert it yourself with strconv.Atoi if you need a number."
  },
  {
    "q": "A request to GET /api/v1/books has no query string. What does c.QueryParam(\"author\") return?",
    "choices": ["An error", "nil", "An empty string \"\"", "It panics on a missing key"],
    "answer": 2,
    "explain": "c.QueryParam never errors on a missing key - it returns \"\". Check for the empty string to decide whether a filter was actually requested."
  }
]
```


---

# Binding & Validation

So far your handlers have read input piece by piece - a path param here, a query string there. That's
fine for a couple of values. But the moment a client `POST`s a JSON body with five fields, picking them
apart by hand gets old fast. This is where **binding** earns its keep: hand Echo a struct, and it fills
it in for you.

Here's the mental model to hold before you touch any code. In Echo, getting data from a request into a
trustworthy struct is **two separate steps**:

1. **Bind** - decode the raw request body into a Go struct. This is purely *shape*: does the JSON parse,
   do the fields line up?
2. **Validate** - check that the now-populated struct actually makes *sense*. Is `Title` non-empty? Is
   the email a real email?

📝 This split is a real difference from Gin. In [Gin](/guides/gin-from-zero), one call with a `binding`
struct tag does both at once. Echo deliberately keeps them apart: `c.Bind` does decoding, and validation
is something *you opt into*. That means a bit more wiring up front - and a lot more clarity about which
step failed when something goes wrong.

We'll keep growing the **books API**, where a book is `Book{id, title, author}`.

## Step one: `c.Bind` decodes the body

`c.Bind(&obj)` reads the request body and decodes it into the struct you point it at. The clever part:
it picks the decoder based on the request's `Content-Type` header. JSON in? It uses the JSON decoder.
A form post? The form decoder. XML? You get the idea. You write one line and Echo handles the format.

The fields it fills come from struct tags - `json:"title"` tells the JSON decoder which key maps to which
field.

```go
type CreateBook struct {
	Title  string `json:"title"`
	Author string `json:"author"`
}

func create(c echo.Context) error {
	var in CreateBook
	if err := c.Bind(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid body")
	}
	return c.JSON(http.StatusOK, in)
}
```

*What just happened:* we declared a struct describing the shape we expect, then called `c.Bind(&in)` - 
note the `&`, Bind needs a pointer to write into. If the body is malformed JSON (or the wrong content
type), `Bind` returns an error and we bail out with a 400. On success, `in` is populated and we echo it
back.

⚠️ One trap worth naming early: `Bind` succeeding does **not** mean the data is good. An empty body that
parses to a zero-value struct, or a JSON object with `{"title":""}`, both bind without error. Bind checks
the envelope, not the contents. That's exactly why the second step exists.

## Step two: wiring up a validator

Here's the thing nobody tells you up front: **Echo has no built-in validator.** There's no magic tag that
rejects empty strings for you. Echo gives you a *hook* - `e.Validator` - and expects you to plug something
into it. The near-universal choice is [go-playground/validator v10](https://github.com/go-playground/validator).

The hook is an interface with a single method, `Validate(i any) error`. You write a tiny adapter that
satisfies it:

```go
import "github.com/go-playground/validator/v10"

type CustomValidator struct {
	v *validator.Validate
}

func (cv *CustomValidator) Validate(i any) error {
	return cv.v.Struct(i)
}
```

*What just happened:* `CustomValidator` wraps a `*validator.Validate` instance. Its `Validate` method just
forwards the struct to `cv.v.Struct(i)`, which inspects the struct's `validate:"..."` tags and returns an
error if any rule fails. That's the entire bridge between Echo and the validator library - write it once
and forget it.

Now register it on your Echo instance in `main`:

```go
func main() {
	e := echo.New()
	e.Validator = &CustomValidator{v: validator.New()}

	e.POST("/books", create)
	e.Logger.Fatal(e.Start(":8080"))
}
```

*What just happened:* setting `e.Validator` is what makes `c.Validate(...)` work inside handlers. Skip
this line and every call to `c.Validate` fails at runtime complaining no validator is registered.
`validator.New()` builds the underlying engine that reads your tags.

With the hook in place, the rules themselves live in `validate:"..."` struct tags. The validator v10 ones
you'll reach for constantly:

- `required` - the field must not be its zero value (empty string, `0`, `nil`).
- `email` - must look like an email address.
- `min` / `max` - for strings, length bounds; for numbers, value bounds (`min=1`).
- `gte` / `lte` - greater/less than or equal, for numbers.
- `oneof` - must be one of a fixed set, e.g. `oneof=fiction nonfiction`.

You can stack them comma-separated: `validate:"required,min=1,max=200"`.

## Putting it together: the create handler

Now the two steps live side by side. Bind, then validate, then act. Here's the full create handler for
the books API:

```go
type CreateBook struct {
	Title  string `json:"title" validate:"required,min=1"`
	Author string `json:"author" validate:"required"`
}

func create(c echo.Context) error {
	var in CreateBook
	if err := c.Bind(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid body")
	}
	if err := c.Validate(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	book := Book{ID: nextID(), Title: in.Title, Author: in.Author}
	// ... persist book ...
	return c.JSON(http.StatusCreated, book)
}
```

*What just happened:* the flow reads top to bottom like a checklist. `c.Bind(&in)` decodes the JSON - fail
here means the body was unparseable, so 400. `c.Validate(&in)` runs the tag rules - fail here means the
body parsed but `Title` was empty or some other rule broke, so also 400, and we hand back the validator's
own message so the client knows *what* was wrong. Only once both pass do we build the real `Book`, assign
it an ID, save it, and return `201 Created`.

💡 Look at how every failure path ends: `return echo.NewHTTPError(...)`. We're not writing the error
response by hand - no `c.JSON(400, ...)` with a hand-rolled body. We *return* the error and let Echo's
central error handler turn it into a response. That's Echo's whole personality: handlers describe *what
went wrong*, one place decides *how it looks*. We'll build that central `HTTPErrorHandler` properly in
[Phase 6](06-rest-api-and-errors.md) - for now, trust that returning an `HTTPError` produces a sensible
JSON error with the right status.

## Recap

- In Echo, **binding and validation are two distinct steps** - unlike Gin, which fuses them. Bind decodes;
  validate checks.
- **`c.Bind(&obj)`** decodes the request body into a struct, choosing the decoder from the `Content-Type`
  header, and maps fields via `json:"..."` tags. Pass a pointer.
- A successful `Bind` only means the body *parsed* - it says nothing about whether the data is valid.
- **Echo ships no validator.** You wire one up: a small `CustomValidator` adapter, register it as
  `e.Validator = ...`, then call `c.Validate(...)` in handlers. Rules live in `validate:"..."` tags
  (`required`, `email`, `min`/`max`, `gte`/`lte`, `oneof`).
- Report bad input by **returning `echo.NewHTTPError(...)`**, letting Echo's central handler render it - 
  don't write error responses by hand.

## Quick check

```quiz
[
  {
    "q": "What does c.Bind(&obj) actually do?",
    "choices": ["Decodes the request body into the struct, picking a decoder from Content-Type", "Decodes the body AND validates it against struct tags", "Only validates the struct, never decodes", "Reads query parameters into the struct"],
    "answer": 0,
    "explain": "c.Bind decodes the body into the struct, choosing JSON/XML/form based on the Content-Type header. It does not validate - that's a separate step in Echo."
  },
  {
    "q": "How do you enable c.Validate(...) in an Echo app?",
    "choices": ["It works automatically; Echo has a built-in validator", "Add a validate: tag to your struct and Echo handles the rest", "Set e.Validator to your own type that implements Validate(i any) error", "Call validator.New() inside every handler"],
    "answer": 2,
    "explain": "Echo has no built-in validator. You implement the Validator interface (a Validate(i any) error method) and register it as e.Validator, commonly wrapping go-playground/validator."
  },
  {
    "q": "When validation fails in the create handler, what's the recommended way to respond?",
    "choices": ["Call c.JSON(400, ...) with a hand-built error body", "panic so Recover middleware catches it", "return echo.NewHTTPError(http.StatusBadRequest, ...) and let the central handler render it", "Ignore it and return 201 anyway"],
    "answer": 2,
    "explain": "Echo's style is to return an HTTPError and let the centralized error handler turn it into a response - keeping handlers focused on what went wrong, not how it looks."
  }
]
```


---

# Responses & Rendering

In [Phase 3](03-binding-and-validation.md) you learned to read input safely. Now the other direction:
getting data back out to the client.

## The mental model: a response is something you *return*

In Echo, every helper that sends a response **also returns an `error`** - and you `return` that value
straight out of your handler. You don't call `c.JSON(...)` and then keep going; you `return c.JSON(...)`.

Think of it as: *pick a helper, pick a status code, return it.* That's the whole shape of a handler's
last line. The framework writes the response and propagates any write error up to Echo's error handler
(which you'll meet properly in [Phase 6](06-rest-api-and-errors.md)). Forgetting that little `return` is
the single most common Echo beginner bug - the response doesn't get sent, or your code runs past the
point where it should have stopped.

> 📝 The status code is always the **first argument** to these helpers, and it always comes from the
> `net/http` constants - `http.StatusOK` (200), `http.StatusCreated` (201), `http.StatusNotFound` (404),
> and friends. Use the named constants, not bare numbers; they read better and they're impossible to
> typo into a wrong-but-valid number.

We'll keep growing the **books API** from earlier phases. Our model stays the same:

```go
type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}
```

*What just happened:* the `json:"..."` struct tags decide the field names Echo writes in the JSON output.
Without them you'd get `ID`, `Title`, `Author` (Go's exported-field casing); with them you get clean
lowercase keys.

## JSON: the workhorse

Most of what you return from an API is JSON, and `c.JSON(status, value)` is how. Echo marshals the value
and sets `Content-Type: application/json` for you. Here are the three status codes you'll reach for most:

```go
// GET /books/:id  → 200 with one book, or 404 if it's missing
e.GET("/books/:id", func(c echo.Context) error {
	id, _ := strconv.Atoi(c.Param("id"))
	book, ok := store[id]
	if !ok {
		return c.JSON(http.StatusNotFound, map[string]string{
			"message": "book not found",
		})
	}
	return c.JSON(http.StatusOK, book)
})

// POST /books  → 201 with the created book
e.POST("/books", func(c echo.Context) error {
	var b Book
	if err := c.Bind(&b); err != nil {
		return err
	}
	b.ID = nextID()
	store[b.ID] = b
	return c.JSON(http.StatusCreated, b)
})
```

*What just happened:* notice every branch ends in `return c.JSON(...)`. The 404 path returns a small
JSON object so the client gets a parseable body, not an empty 404. The create path returns **201 Created**
(not 200) with the new book, including its freshly assigned `ID` - the main reason clients want the
created object echoed back.

Returning a **list** is the same call - just hand it a slice:

```go
// GET /books  → 200 with all books as a JSON array
e.GET("/books", func(c echo.Context) error {
	books := make([]Book, 0, len(store))
	for _, b := range store {
		books = append(books, b)
	}
	return c.JSON(http.StatusOK, books)
})
```

*What just happened:* we build the slice with `make([]Book, 0, ...)` rather than a `nil` slice. That
matters: a `nil` slice marshals to `null`, but an empty initialized slice marshals to `[]` - clients
iterating the response would rather get an empty array than `null`.

> 💡 During development, `c.JSONPretty(status, value, "  ")` indents the output so you can read it in a
> terminal. In production, stick with plain `c.JSON` - the extra whitespace is wasted bytes over the wire.

## The other response helpers

JSON isn't the only way to answer. Each of these follows the same return-an-error rule:

```go
// Plain text
return c.String(http.StatusOK, "pong")

// Raw HTML string (not a template - just a string of HTML)
return c.HTML(http.StatusOK, "<h1>Books</h1>")

// Raw bytes with a content type you choose
return c.Blob(http.StatusOK, "text/csv", []byte("id,title\n1,Dune\n"))

// Stream a file from disk (sets content type from the extension)
return c.File("reports/books.pdf")

// Redirect the browser elsewhere
return c.Redirect(http.StatusFound, "/books")

// Empty body - perfect for DELETE
return c.NoContent(http.StatusNoContent)
```

*What just happened:* each helper picks the right `Content-Type` for its job (`c.String` → text/plain,
`c.Blob` → whatever you pass, `c.File` → guessed from the extension) and writes the status. `c.NoContent`
is the one to remember: it sends a status with **no body at all**, exactly what a successful `DELETE`
should return.

Here's the clean DELETE that ties it together:

```go
// DELETE /books/:id  → 204 on success, 404 if it wasn't there
e.DELETE("/books/:id", func(c echo.Context) error {
	id, _ := strconv.Atoi(c.Param("id"))
	if _, ok := store[id]; !ok {
		return c.JSON(http.StatusNotFound, map[string]string{"message": "book not found"})
	}
	delete(store, id)
	return c.NoContent(http.StatusNoContent)
})
```

*What just happened:* on success there's nothing meaningful to send back - the resource is gone - so
`c.NoContent(http.StatusNoContent)` returns **204** with an empty body.

### Setting your own headers

Sometimes you need to add a header before you send the body - a cache directive, a custom `X-` header, a
location. You reach through to the underlying response writer:

```go
e.GET("/books/:id", func(c echo.Context) error {
	id, _ := strconv.Atoi(c.Param("id"))
	book, ok := store[id]
	if !ok {
		return c.NoContent(http.StatusNotFound)
	}
	c.Response().Header().Set("X-Resource-Version", "1")
	return c.JSON(http.StatusOK, book)
})
```

*What just happened:* `c.Response().Header().Set("X-Resource-Version", "1")` sets a header. The order
matters - set headers **before** the response helper, since that helper writes the status line and
flushes headers. Set one after `c.JSON(...)` and it's too late; the bytes are already going out.

## HTML templates: the Renderer interface

Not every Echo app is an API - sometimes you render server-side HTML. Unlike some frameworks, Echo
doesn't ship a built-in template engine. Instead it defines a small interface and lets you plug in
whatever you like (almost always Go's standard `html/template`).

The interface is one method:

```go
type Renderer interface {
	Render(w io.Writer, name string, data any, c echo.Context) error
}
```

So you write a tiny type that satisfies it by wrapping `html/template`, assign it to `e.Renderer`, and
then call `c.Render(...)` in handlers. Here's the whole setup:

```go
import (
	"html/template"
	"io"

	"github.com/labstack/echo/v4"
)

// Template wraps a parsed set of html/template files and satisfies echo.Renderer.
type Template struct {
	templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data any, c echo.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
	e := echo.New()

	// Parse every .html file in views/ once at startup.
	e.Renderer = &Template{
		templates: template.Must(template.ParseGlob("views/*.html")),
	}

	e.GET("/books", func(c echo.Context) error {
		books := []Book{
			{ID: 1, Title: "Dune", Author: "Herbert"},
			{ID: 2, Title: "Neuromancer", Author: "Gibson"},
		}
		return c.Render(http.StatusOK, "books.html", books)
	})

	e.Logger.Fatal(e.Start(":1323"))
}
```

*What just happened:* `template.Must(template.ParseGlob(...))` parses all your templates once at boot and
panics if any fail to parse - a broken template should stop startup, not surface as a runtime surprise.
We hang that on `e.Renderer`. From then on `c.Render(status, "books.html", data)` runs `Render`, which
calls `ExecuteTemplate` with the data - here, our slice of books.

And the template file itself:

```html
<!-- views/books.html -->
<h1>Books</h1>
<ul>
  {{range .}}
    <li>{{.Title}} - {{.Author}}</li>
  {{end}}
</ul>
```

*What just happened:* the `data` you passed to `c.Render` arrives as `.` (dot) inside the template.
`{{range .}}` loops the slice; inside the loop, `.` is each `Book`, so `{{.Title}}` and `{{.Author}}`
pull its fields. Crucially, `html/template` **auto-escapes** these values - a title of
`<script>alert(1)</script>` renders as harmless text, not executable script. That's the whole reason to
use `html/template` and not string concatenation for HTML.

## Static files

For CSS, JavaScript, images, and other on-disk assets, Echo serves directories and single files directly:

```go
// Serve everything in the local "assets" dir under the /assets URL prefix.
// A request for /assets/app.css returns ./assets/app.css.
e.Static("/assets", "assets")

// Serve one specific file at one specific URL.
e.File("/favicon.ico", "images/favicon.ico")
```

*What just happened:* `e.Static(prefix, root)` maps a URL prefix to a folder on disk - great for a whole
`assets/` tree. `e.File(path, file)` wires a single URL to a single file, for one-offs like a favicon
that doesn't live where the URL implies.

> 💡 Most Echo services in the wild are pure JSON APIs - they use `c.JSON` and little else, and never
> touch a Renderer or static files at all. Templates and static serving are there when you need a
> server-rendered page or a small bundled front-end, but don't feel you must reach for them. Reach for
> the response helper that fits the job.

## Recap

- Every response helper **returns an `error` you return** - the handler's last line is
  `return c.Something(...)`. Forgetting the `return` is the classic Echo bug.
- `c.JSON(status, value)` is the API workhorse: **201** for creates, **404** for missing,
  and pair an empty initialized slice with 200 so lists serialize as `[]`, not `null`.
- `c.NoContent(http.StatusNoContent)` is the clean **204** answer for a successful `DELETE` - status, no body.
- Other helpers - `c.String`, `c.HTML`, `c.Blob`, `c.File`, `c.Redirect` - each set the right content type;
  set custom headers via `c.Response().Header().Set(...)` **before** sending.
- HTML rendering needs a Renderer: implement `echo.Renderer`, assign `e.Renderer`, call `c.Render`;
  `html/template` auto-escapes your data. Serve assets with `e.Static` / `e.File`.

## Quick check

```quiz
[
  {
    "q": "What's the idiomatic Echo response for a successful DELETE that has nothing to return?",
    "choices": ["c.JSON(http.StatusOK, nil)", "c.String(http.StatusOK, \"\")", "c.NoContent(http.StatusNoContent)", "return nil with no helper call"],
    "answer": 2,
    "explain": "c.NoContent(http.StatusNoContent) sends a 204 status with an empty body - exactly right for a successful delete."
  },
  {
    "q": "Why must you write `return c.JSON(...)` rather than just `c.JSON(...)`?",
    "choices": ["c.JSON returns an error that Echo expects you to propagate", "It runs faster", "Go requires return on the last line", "Without return the JSON is double-encoded"],
    "answer": 0,
    "explain": "Every response helper returns an error; returning it lets Echo's error handling work and stops the handler at the right point."
  },
  {
    "q": "How do you enable HTML template rendering in Echo?",
    "choices": ["Call e.EnableTemplates()", "Implement echo.Renderer, assign it to e.Renderer, then call c.Render", "Pass templates to echo.New()", "Use c.HTML with a file path"],
    "answer": 1,
    "explain": "Echo has no built-in engine: you implement the Renderer interface (usually wrapping html/template), set e.Renderer, and call c.Render."
  }
]
```


---

# Middleware

Here's the part of a web framework that earns its keep: not the routing, but everything that runs *around* every request. Logging. Auth. Panic recovery. Timing. CORS headers. You don't want to paste those into all forty of your handlers - you write them once and have them wrap the whole app.

That wrapper is **middleware**. Echo's take on it is a little different from Gin's, and once the shape clicks, every middleware pattern in Echo follows it.

## The mental model: a function that wraps a handler

> 💡 In Echo, a middleware is a function that *takes* your handler and *returns* a new handler that wraps it. It's literally `func(next) handler`. You decide what happens before you call `next`, what happens after, and whether you call `next` at all.

Picture an onion. Your handler sits in the center. Each middleware is a layer wrapped around it: the request travels inward through every layer to reach your handler, then the result travels back outward through those same layers. The thing that travels back out is the **`error`** your handler returned - and because each middleware *calls* `next(c)` and gets that error back, every layer can inspect or transform it on the way out.

```mermaid
flowchart LR
  R[Request] --> L1[Logger wraps]
  L1 --> A1[Auth wraps]
  A1 --> H[Your handler returns error]
  H --> A2[error flows back through Auth]
  A2 --> L2[error flows back through Logger]
  L2 --> Resp[Response]
```

Gin gives you one flat handler and a `c.Next()` seam to mark "before vs. after." Echo gives you a *wrapper* - you hold a reference to `next` and call it yourself. The "before" code is whatever you write before that call; the "after" code is whatever you write after it.

## The signature, and the three ways to register

A middleware in Echo is an `echo.MiddlewareFunc`, which is exactly this type:

```go
type MiddlewareFunc func(next echo.HandlerFunc) echo.HandlerFunc
```

So you write a function that receives the `next` handler and returns a replacement handler. The classic example is a request timer:

```go
func Timer(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        start := time.Now()
        err := next(c)               // run the rest of the chain
        log.Printf("%s %s %v", c.Request().Method, c.Path(), time.Since(start))
        return err
    }
}
```

*What just happened:* `Timer` takes `next` and hands back a brand-new `echo.HandlerFunc`. Inside it, `start := time.Now()` runs on the way *in*. The call `err := next(c)` runs everything deeper in the chain - your actual handler writes its response and returns an error (often `nil`). Only *then* does `log.Printf` run, capturing the full duration. Finally we `return err` so the error keeps flowing outward to whatever wrapped *us*. Forwarding that `err` isn't optional - swallow it and Echo's error handler never sees the failure.

You attach a middleware in one of three scopes:

```go
e := echo.New()

// 1. Global - runs for EVERY route on this instance.
e.Use(Timer)

// 2. Per-group - runs only for routes in this group.
api := e.Group("/api/v1")
api.Use(Timer)
// (or pass it when you create the group:)
admin := e.Group("/admin", Timer)

// 3. Per-route - runs only for this one route. List middleware after the handler.
e.GET("/health", healthHandler, Timer)
```

*What just happened:* same middleware, three reaches. `e.Use` wraps the whole app; `group.Use` (or passing it to `e.Group`) wraps a subtree of routes - this is how you protect `/api/v1/*` without touching public routes; listing it after the handler in `e.GET` wraps exactly one endpoint. Pick the narrowest scope that does the job. Notice you pass `Timer` itself, not `Timer()` - Echo's middleware *is* the function, you're not calling it to produce one.

> 📝 Order matters. Middleware runs in the order you register it. `e.Use(A); e.Use(B)` makes A the outermost layer: A's "before" code runs first, and A's "after" code runs *last*, because A wraps B which wraps your handler.

## The built-ins: opt in to what you need

Echo ships a generous set of middleware in `github.com/labstack/echo/v4/middleware`. Unlike some frameworks, Echo adds **none** of them by default - `echo.New()` gives you a bare instance, and you opt in to each one. The ones you'll reach for first:

```go
import "github.com/labstack/echo/v4/middleware"

e := echo.New()
e.Use(middleware.Logger())   // logs method, path, status, latency per request
e.Use(middleware.Recover())  // catches panics, logs the stack, returns 500
e.Use(middleware.CORS())     // adds CORS headers for browser cross-origin calls
e.Use(middleware.Gzip())     // gzip-compresses responses
```

*What just happened:* four lines, four cross-cutting concerns handled for the whole app. `Logger()` is the per-request line you see in your terminal. `Recover()` is the one you should almost never skip - without it, a single nil-pointer dereference in any handler panics and kills the process for *every* user; with it, that one request gets a 500 and the server keeps serving. `CORS()` and `Gzip()` are situational (add CORS when a browser front-end on another origin calls your API). There are more in the same package - `middleware.RateLimiter(...)` to throttle clients, plus `middleware.JWT(...)` and `middleware.BasicAuth(...)` for authentication.

> ⚠️ Because Echo opts you in to nothing, you can ship a server with no panic recovery and never notice. If you write `echo.New()` and stop there, one panic anywhere takes the whole process down. Add `middleware.Recover()` unless you have a deliberate reason not to.

## Writing an auth middleware for the books group

Now the real one. Most custom middleware you write will be an auth check: read a credential, reject the request if it's missing or bad, otherwise stash who the user is and let the request through. In Echo you reject by **returning** an error - specifically `echo.NewHTTPError(401, ...)` - and you pass data downstream with `c.Set` / `c.Get`.

```go
func Auth(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        token := c.Request().Header.Get("Authorization")
        if token == "" {
            // Return the error - DON'T call next. The chain stops here.
            return echo.NewHTTPError(http.StatusUnauthorized, "missing Authorization header")
        }

        user := lookupUser(token) // returns "" if the token is invalid
        if user == "" {
            return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
        }

        c.Set("user", user) // hand the user to downstream handlers
        return next(c)       // let the request through
    }
}
```

*What just happened:* this is the whole pattern. There's no separate "abort" call like Gin's `c.Abort()` - in Echo you stop the chain by **not calling `next(c)`** and returning an error instead. Both failure paths `return echo.NewHTTPError(...)`, which Echo's error handler turns into a clean JSON 401 (you'll meet that central handler in the next phase). On success we `c.Set("user", user)` to stash the user, then `return next(c)` to run the rest of the chain - the handler downstream never re-does auth.

Now wire it onto the `/api/v1` books group and read the user inside a handler:

```go
func main() {
    e := echo.New()
    e.Use(middleware.Logger(), middleware.Recover())

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

    books := api.Group("/books")
    books.GET("", listBooks)

    e.Logger.Fatal(e.Start(":8080"))
}

func listBooks(c echo.Context) error {
    user := c.Get("user").(string) // we KNOW Auth ran and set this
    return c.JSON(http.StatusOK, echo.Map{
        "user":  user,
        "books": []string{"The Go Programming Language", "Designing Data-Intensive Applications"},
    })
}
```

*What just happened:* because `Auth` is attached to the `api` group, every route under `/api/v1` is protected - no token, no entry. Inside `listBooks`, `c.Get("user")` retrieves what the middleware stashed; the `.(string)` is a type assertion because `Get` returns `any`. We assert directly because `Auth` guarantees the value is there - if you weren't certain, you'd guard it: `user, ok := c.Get("user").(string)`.

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

> 💡 Coming from Gin? Gin middleware is a flat `func(c *gin.Context)` that calls `c.Next()` to continue and `c.Abort()` to stop. Echo middleware *wraps* - it receives `next` and **calls it** to continue, or **doesn't call it (and returns an error)** to stop. Same onion, different mechanics. Once you internalize "call `next` to go deeper, return an error to bail," Echo middleware stops feeling foreign.

## Recap

- A middleware is an `echo.MiddlewareFunc` = `func(next echo.HandlerFunc) echo.HandlerFunc`: it receives the next handler and returns a wrapper. Code before `next(c)` runs on the way in; code after runs on the way out.
- Register it three ways: `e.Use()` (global), `group.Use()` or `e.Group(path, mw)` (a subtree like `/api/v1`), or after the handler on a route (`e.GET("/x", h, mw)`). Registration order is execution order.
- Always `return` the error from `next(c)` so failures keep flowing out to Echo's error handler - don't swallow it.
- Echo adds **no** middleware by default. Opt in to built-ins from `.../echo/v4/middleware`: `Logger()`, `Recover()`, `CORS()`, `Gzip()`, `RateLimiter()`, `JWT()`, `BasicAuth()`. Keep `Recover()` unless you really mean to drop it.
- Stop the chain by **not calling `next` and returning `echo.NewHTTPError(code, msg)`** - there's no `Abort`. Pass data downstream with `c.Set("user", v)` and read it with `c.Get("user")`.
- Versus Gin: Gin calls `c.Next()` / `c.Abort()` inside one flat handler; Echo wraps `next` and either calls it or returns an error.

## Quick check

Lock in the one idea that matters most - the wrap-and-return shape:

```quiz
[
  {
    "q": "What is an echo.MiddlewareFunc?",
    "choices": ["func(c echo.Context)", "func(next echo.HandlerFunc) echo.HandlerFunc", "func(c echo.Context) error", "An interface with a Handle method"],
    "answer": 1,
    "explain": "Echo middleware takes the next handler and returns a new handler that wraps it: func(next echo.HandlerFunc) echo.HandlerFunc. You call next(c) to continue the chain."
  },
  {
    "q": "In an Echo middleware, how do you stop the chain when auth fails?",
    "choices": ["Call c.Abort()", "Call next(nil)", "Don't call next(c) - return echo.NewHTTPError(401, ...) instead", "Call c.Next() with a 401"],
    "answer": 2,
    "explain": "Echo has no Abort. You stop the chain by not calling next(c) and returning an error; echo.NewHTTPError lets the central error handler render a clean 401."
  },
  {
    "q": "Which built-in middleware does echo.New() attach for you automatically?",
    "choices": ["Logger and Recover", "Recover only", "CORS and Gzip", "None - Echo opts you in to all of them"],
    "answer": 3,
    "explain": "Echo adds no middleware by default. You opt in with e.Use(middleware.Logger()), middleware.Recover(), etc. Keep Recover() unless you have a deliberate reason not to."
  }
]
```


---

# A REST API with Error Handling

This is the phase where everything from the last five clicks into place. You've got routing, groups,
binding, validation, responses, and middleware. Now we assemble them into a real REST resource - and
lean hard on a feature that's been quietly waiting since Phase 3: Echo's centralized error handling.

Here's the mental model to carry through this whole phase, two ideas held together:

1. **A REST resource is five handlers over one collection.** For `books`, that's: list them all, get
   one, create one, update one, delete one. Five verbs, one slice of the world. Every CRUD API you'll
   ever write is this same shape repeated.
2. **In Echo, every failure path is "return an error."** A handler doesn't write a 404 by hand. It
   `return`s an error, and *one* place - the `HTTPErrorHandler` - decides how that error looks to the
   client. Your handlers describe *what went wrong*; the central handler decides *how it's rendered*.

Hold both at once and the code almost writes itself: five small functions, each doing its work and
either returning JSON on success or returning an error on failure. No handler ever hand-rolls an error
response.

We'll keep building the **books API**, where a book is `Book{id, title, author}`.

## The in-memory store

Before the handlers, we need somewhere to keep books. We'll use a plain map for now - no database, no
file, just memory. That keeps the focus on Echo instead of SQL.

```go
type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

type Store struct {
	mu     sync.RWMutex
	books  map[int]Book
	nextID int
}

func NewStore() *Store {
	return &Store{books: make(map[int]Book), nextID: 1}
}
```

*What just happened:* `Book` is the resource as the client sees it - note the `json:"..."` tags so it
serializes with lowercase keys. `Store` holds a `map[int]Book` keyed by ID, a `nextID` counter for
assigning new IDs, and - the important part - a `sync.RWMutex`. `NewStore` hands back a ready-to-use,
empty store with IDs starting at 1.

⚠️ That mutex is not optional, and this is the trap that bites people who skip it. **Echo serves
requests concurrently** - each request runs in its own goroutine. If two requests touch `books` at the
same moment (one reading while another writes), Go's map will panic with `fatal error: concurrent map
read and map write`, unrecoverable - not even the Recover middleware from Phase 5 catches it. The fix
is to guard every access: a *read* lock (`RLock`) when only reading, a full *write* lock (`Lock`) when
modifying. We'll do exactly that in each handler below.

## The five handlers

Now the heart of it. Each handler hangs off a versioned group - `g := e.Group("/api/v1")` - so all five
live under `/api/v1/books`. Each one does its work and ends one of two ways: `return c.JSON(...)` on
success, or `return echo.NewHTTPError(...)` on failure. Watch how that single rule plays out five times.

### List - `GET /api/v1/books`

```go
func (s *Store) list(c echo.Context) error {
	s.mu.RLock()
	defer s.mu.RUnlock()

	out := make([]Book, 0, len(s.books))
	for _, b := range s.books {
		out = append(out, b)
	}
	return c.JSON(http.StatusOK, out)
}
```

*What just happened:* we take a read lock (`RLock`) because we're only reading - multiple list requests
can run at once without blocking each other, the whole point of `RWMutex`. We build a slice from the
map's values and return it as JSON with `200 OK`. `make([]Book, 0, ...)` matters: it guarantees an
empty store serializes to `[]`, not `null`.

### Get one - `GET /api/v1/books/:id`

```go
func (s *Store) get(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "id must be a number")
	}

	s.mu.RLock()
	book, ok := s.books[id]
	s.mu.RUnlock()

	if !ok {
		return echo.NewHTTPError(http.StatusNotFound, "book not found")
	}
	return c.JSON(http.StatusOK, book)
}
```

*What just happened:* we pull `:id` from the path (always a string) and convert it with `strconv.Atoi`.
A non-numeric id is the client's mistake, so we return a `400`. Then we look the book up under a read
lock. The comma-ok idiom (`book, ok := s.books[id]`) tells us whether it existed - if not, we
`return echo.NewHTTPError(http.StatusNotFound, "book not found")` rather than writing the 404 ourselves.
On a hit, `200` with the book.

### Create - `POST /api/v1/books`

This is where Phase 3 comes back. Bind, validate, *then* act.

```go
type CreateBook struct {
	Title  string `json:"title"  validate:"required,min=1"`
	Author string `json:"author" validate:"required"`
}

func (s *Store) create(c echo.Context) error {
	var in CreateBook
	if err := c.Bind(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid body")
	}
	if err := c.Validate(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	s.mu.Lock()
	book := Book{ID: s.nextID, Title: in.Title, Author: in.Author}
	s.books[book.ID] = book
	s.nextID++
	s.mu.Unlock()

	return c.JSON(http.StatusCreated, book)
}
```

*What just happened:* same two-step gate from Phase 3 - `c.Bind` decodes the JSON, `c.Validate` runs
the `validate:"..."` rules, and either failure returns a `400` describing what broke. Only once both
pass do we take a full **write** lock (`Lock`, not `RLock` - we're mutating the map *and* the counter),
build the `Book` with the next ID, store it, bump the counter, and unlock. Success returns `201 Created`
with the new resource so the client learns its assigned ID.

### Update - `PUT /api/v1/books/:id`

```go
func (s *Store) update(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "id must be a number")
	}

	var in CreateBook
	if err := c.Bind(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid body")
	}
	if err := c.Validate(&in); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

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

	if _, ok := s.books[id]; !ok {
		return echo.NewHTTPError(http.StatusNotFound, "book not found")
	}
	updated := Book{ID: id, Title: in.Title, Author: in.Author}
	s.books[id] = updated
	return c.JSON(http.StatusOK, updated)
}
```

*What just happened:* update is get-and-create fused. We parse the id, bind+validate the new values
(reusing the same `CreateBook` struct - no second type needed), then take a write lock. We check the
book exists first; missing means `404`. If it's there, we overwrite it - keeping the original `id` so
it stays addressable - and return `200` with the updated record. `defer s.mu.Unlock()` is safe here
even on an early return; `defer` runs on every exit path, including the 404.

### Delete - `DELETE /api/v1/books/:id`

```go
func (s *Store) delete(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "id must be a number")
	}

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

	if _, ok := s.books[id]; !ok {
		return echo.NewHTTPError(http.StatusNotFound, "book not found")
	}
	delete(s.books, id)
	return c.NoContent(http.StatusNoContent)
}
```

*What just happened:* parse the id, take a write lock, confirm the book exists (`404` if not), then
`delete(s.books, id)`. The success response is `c.NoContent(http.StatusNoContent)` - a `204` with an
empty body, the conventional answer to a successful delete.

### Wiring them up

All five mount on the `/api/v1` group in `main`:

```go
func main() {
	e := echo.New()
	e.Validator = &CustomValidator{v: validator.New()} // from Phase 3

	s := NewStore()
	g := e.Group("/api/v1")
	g.GET("/books", s.list)
	g.GET("/books/:id", s.get)
	g.POST("/books", s.create)
	g.PUT("/books/:id", s.update)
	g.DELETE("/books/:id", s.delete)

	e.Logger.Fatal(e.Start(":8080"))
}
```

*What just happened:* one group, five routes, each pointing at a method on the shared `Store`. Because
the handlers are methods on `*Store`, they all close over the same map and mutex - that's how five
independent functions cooperate on one collection. We register the Phase 3 validator so `c.Validate`
works. Notice what's *not* here yet: any error-handling code. Echo already turns returned `HTTPError`s
into JSON by default - but the default shape isn't quite what we want, so let's take control.

## The payoff: a centralized `HTTPErrorHandler`

This is Echo's signature feature, the thing that makes all that `return echo.NewHTTPError(...)`
discipline pay off. Every error your handlers return - plus any error Echo itself raises (a 404 for an
unknown route, a 405 for the wrong method) - funnels through **one function**: `e.HTTPErrorHandler`.
Change that one function and you've changed how *every* error in the entire API looks.

By default, Echo's handler renders an `*echo.HTTPError` as `{"message": "..."}` with the right status,
and turns any *other* error into a generic `500`. That's fine, but say your API has standardized on
`{"error": "..."}` instead. Rather than touch thirty handlers, you write the shape once:

```go
e.HTTPErrorHandler = func(err error, c echo.Context) {
	code := http.StatusInternalServerError
	msg := "internal error"
	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
		msg = fmt.Sprintf("%v", he.Message)
	}
	c.JSON(code, map[string]string{"error": msg})
}
```

*What just happened:* this function receives every error a handler returns. We start with safe defaults
 - `500` and a vague `"internal error"`, because an *unexpected* error (a nil-pointer deref, a failed
DB call later) should never leak its guts to the client. Then we type-assert: if the error is an
`*echo.HTTPError` (what `echo.NewHTTPError` produces), we trust its `Code` and `Message`, because *we*
chose those deliberately. Finally we render one consistent JSON body: `{"error": "..."}`, every time. A
`book not found`, a malformed `id`, and a deep `500` all come out the same shape - only status and
message differ. Register it in `main` alongside the validator:

```go
e.HTTPErrorHandler = customErrorHandler // the function above
```

💡 Across five handlers we wrote `return echo.NewHTTPError(...)` maybe eight times, and *zero* lines of
response-formatting code in any of them. The handlers stayed pure business logic - look up, check, act.
All the "what does an error look like on the wire" logic lives in one ten-line function. That's the
difference between an API that stays clean at fifty endpoints and one that rots into copy-pasted
`c.JSON(400, ...)` calls everywhere.

## Taking it for a spin

Start the server, then exercise the books API with a few `curl` calls:

```bash
# Create a book → 201
curl -s -X POST localhost:8080/api/v1/books \
  -H 'Content-Type: application/json' \
  -d '{"title":"Dune","author":"Herbert"}'
# {"id":1,"title":"Dune","author":"Herbert"}

# List them → 200
curl -s localhost:8080/api/v1/books
# [{"id":1,"title":"Dune","author":"Herbert"}]

# Get one that doesn't exist → 404, your custom shape
curl -s localhost:8080/api/v1/books/999
# {"error":"book not found"}

# Create with an empty title → 400, validator's message
curl -s -X POST localhost:8080/api/v1/books \
  -H 'Content-Type: application/json' \
  -d '{"title":"","author":"Nobody"}'
# {"error":"Key: 'CreateBook.Title' Error:Field validation for 'Title' failed on the 'required' tag"}

# Delete it → 204, empty body
curl -s -i -X DELETE localhost:8080/api/v1/books/1 | head -n 1
# HTTP/1.1 204 No Content
```

*What just happened:* the happy paths return the resource as JSON with the right status. The two failure
paths - a missing book and a validation miss - both come back in your `{"error": ...}` shape, even
though one originated in a handler's `NewHTTPError` and the other in the validator. That uniformity is
the centralized handler doing its job.

💡 Notice the store was the *only* part tied to memory. Every handler talks to `Store`, never to a map
directly - so when you outgrow in-memory storage, you swap `Store`'s guts for a database and the five
handlers don't change a line. [GORM From Zero](/guides/gorm-from-zero) shows how to back this exact API
with a real SQL table. Same handlers, same error handling, real persistence underneath.

## Recap

- **A REST resource is five handlers over one collection**: list (`200`), get (`200`/`404`), create
  (`201`), update (`200`/`404`), delete (`204`). The books API is this shape, mounted on a
  `g := e.Group("/api/v1")`.
- The in-memory `Store` is a `map[int]Book` plus a `sync.RWMutex` and a `nextID`. ⚠️ Echo serves
  requests concurrently - guard reads with `RLock` and writes with `Lock`, or an unguarded map will
  panic unrecoverably.
- Handlers stay clean by following one rule: **bind → validate → do work → `return c.JSON(...)` or
  `return echo.NewHTTPError(...)`**. They never hand-roll an error response.
- The **centralized `HTTPErrorHandler`** is Echo's signature feature: one function turns every returned
  error into one consistent shape (here `{"error": ...}`), with safe `500` defaults for unexpected
  errors and trusted codes/messages for `*echo.HTTPError`.
- Because every handler talks to `Store` and never to a map directly, you can swap the store for a real
  database ([GORM From Zero](/guides/gorm-from-zero)) without touching a single handler.

## Quick check

```quiz
[
  {
    "q": "Why does the in-memory Store need a sync.RWMutex?",
    "choices": ["To make the JSON serialize faster", "Because Echo serves requests concurrently, and an unguarded map read+write panics", "Because echo.NewHTTPError requires a locked store", "It's optional; maps are already concurrency-safe in Go"],
    "answer": 1,
    "explain": "Echo runs each request in its own goroutine. Concurrent read and write on a plain Go map causes an unrecoverable fatal error, so shared state must be guarded with a mutex - RLock for reads, Lock for writes."
  },
  {
    "q": "In an Echo handler, how should you report that a book wasn't found?",
    "choices": ["Call c.JSON(404, ...) with a hand-built error body", "panic(\"not found\") and let Recover handle it", "return echo.NewHTTPError(http.StatusNotFound, \"book not found\")", "Return nil and set the status separately"],
    "answer": 2,
    "explain": "The Echo style is to return an HTTPError. Handlers describe what went wrong; the centralized HTTPErrorHandler decides how it's rendered - so handlers never hand-roll error responses."
  },
  {
    "q": "What does customizing e.HTTPErrorHandler give you?",
    "choices": ["One place that turns every returned error into one consistent response shape for the whole API", "Automatic validation of every request body", "Per-route error formatting that each handler configures itself", "Faster routing for the /api/v1 group"],
    "answer": 0,
    "explain": "The HTTPErrorHandler is a single function every error funnels through. Change it once and every error in the API - from handlers and from Echo itself - comes out in the same shape, with safe 500 defaults for unexpected errors."
  }
]
```


---

# Testing & Production

You've grown the books API from a single route into a real CRUD service with a centralized error handler. Two questions are left, and they decide whether anyone runs this in anger: can you *prove* it works, and can you run it somewhere real without it dying during a 3am deploy? Both answers are smaller than you'd expect, because both rest on the same fact about what Echo actually is.

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

Here's the fact that makes everything in this phase easy. An `*echo.Echo` - the instance you get from `echo.New()` - satisfies Go's `http.Handler` interface. It has a `ServeHTTP(w, r)` method - the *exact* same interface the standard library's `http.Server` uses to feed it live requests off a socket.

> 💡 If your router is an `http.Handler`, a test is nothing more than calling `ServeHTTP` yourself with a fake request and a fake response writer. No network. No port. No goroutine running a server in the background. You hand Echo 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 (`rec.Code`, `rec.Body`).

Wire those together with the same router your `main` uses, 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"
)

func TestListBooks(t *testing.T) {
    e := setupRouter()

    req := httptest.NewRequest(http.MethodGet, "/api/v1/books", nil)
    rec := httptest.NewRecorder()
    e.ServeHTTP(rec, req)

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

    var got []Book
    if err := json.Unmarshal(rec.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 in a second), made a recorder and a GET request for `/api/v1/books`, and called `e.ServeHTTP(rec, req)`. That one call runs the *entire* chain - middleware, routing, your handler, and Echo's error handler - exactly as a live request would, except nothing left the process. Afterward `rec.Code` holds the status and `rec.Body` is a `*bytes.Buffer` with the response body, which we unmarshal to confirm the JSON shape.

Testing a **POST** is the same shape with two additions: pass a body, and set the content type so Echo's `c.Bind` knows it's JSON.

```go
func TestCreateBook(t *testing.T) {
    e := setupRouter()

    body := `{"title":"The Go Programming Language","author":"Donovan & Kernighan"}`
    req := httptest.NewRequest(http.MethodPost, "/api/v1/books", strings.NewReader(body))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    rec := httptest.NewRecorder()
    e.ServeHTTP(rec, req)

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

*What just happened:* `strings.NewReader(body)` turns our JSON string into an `io.Reader`, which is what the request body wants. Setting the content type via Echo's own constants (`echo.HeaderContentType` and `echo.MIMEApplicationJSON` - just `"Content-Type"` and `"application/json"` spelled safely) matters - without it, `c.Bind` won't treat the body as JSON, and you'd be testing the wrong path. We assert `201 Created`. (To test 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 an Echo app. The rest - table-driven cases, golden files, running it all on every push - is general Go testing, covered in [testing in CI](/guides/testing-in-ci).

## Two test styles, and the `setupRouter()` you build once

There are two ways to drive Echo in a test, and it's worth knowing both because they answer different questions.

The **full-router style** is what you saw above: `e.ServeHTTP(rec, req)`. It exercises the *real* path - routing matches the URL, every middleware runs, the error handler fires. This is what you want most of the time, because it tests the wiring, not just the function.

The **handler-level style** skips routing and middleware and calls one handler directly. You build a context by hand with `e.NewContext`, invoke the handler, and inspect the recorder:

```go
func TestGetBookHandler(t *testing.T) {
    e := echo.New()
    req := httptest.NewRequest(http.MethodGet, "/", nil)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)
    c.SetParamNames("id")
    c.SetParamValues("1")

    if err := getBook(c); err != nil {
        t.Fatalf("handler returned error: %v", err)
    }
    if rec.Code != http.StatusOK {
        t.Fatalf("got %d, want 200", rec.Code)
    }
}
```

*What just happened:* `e.NewContext(req, rec)` builds an `echo.Context` wired to our fake request and recorder, without going through the router - so we set the path param ourselves with `SetParamNames`/`SetParamValues` (the router would normally fill those in). Then we call `getBook(c)` straight and check both its returned `error` and `rec.Code`. Because a handler is `func(c echo.Context) error`, check the *return value too*, not only the recorder. Use this style for focused unit tests; reach for `ServeHTTP` when you want to know the route and middleware actually line up.

Both styles need one structural discipline to stay clean: **factor your router construction into a function**, conventionally `setupRouter()`, that returns the configured `*echo.Echo`. Both `main` and your tests call it, so they exercise the *same* wiring.

```go
func setupRouter() *echo.Echo {
    e := echo.New()
    e.HTTPErrorHandler = customErrorHandler // from Phase 6

    v1 := e.Group("/api/v1")
    v1.GET("/books", listBooks)
    v1.POST("/books", createBook)
    v1.GET("/books/:id", getBook)
    v1.PUT("/books/:id", updateBook)
    v1.DELETE("/books/:id", deleteBook)

    return e
}

func main() {
    e := setupRouter()
    e.Logger.Fatal(e.Start(":8080"))
}
```

*What just happened:* all route and middleware registration lives in one place. `main` builds the instance and starts it; a test builds the *same* instance and pokes it with `httptest` - one source of truth, no second, slightly-different route set quietly drifting. (If your handlers need a database or config, have `setupRouter(deps)` take them as parameters so tests can pass fakes.)

## Production niceties: quiet the banner, set real timeouts

Echo prints a friendly startup banner and a "server started on..." line by default. Lovely in dev, noise in production logs. Turn both off on the instance:

```go
e := echo.New()
e.HideBanner = true
e.HidePort = true
```

*What just happened:* `HideBanner` suppresses the ASCII Echo logo at startup, and `HidePort` drops the "⇨ http server started on [::]:8080" line. Neither touches your application logging or the Recover/Logger middleware - you're silencing Echo's cosmetic chatter, not going dark.

The more important production setting is **server timeouts**. Echo exposes the underlying `*http.Server` as `e.Server`, so you set them directly:

```go
e.Server.ReadTimeout = 5 * time.Second
e.Server.WriteTimeout = 10 * time.Second
```

*What just happened:* by default an `http.Server` has *no* timeouts, so 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. `ReadTimeout` caps how long reading the request is allowed to take; `WriteTimeout` caps the response. This is a baseline defense every real server makes.

## Graceful shutdown: why `e.Start()` alone isn't enough

`e.Start(":8080")` blocks and serves forever, exactly right for learning. For a real deploy it has one gap: when your platform restarts the service (a deploy, a scale-down, a `SIGTERM`), `e.Start()` 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* - a **graceful shutdown**.

Echo gives you `e.Shutdown(ctx)` for exactly this. The pattern: start the server in a goroutine so `main` is free to wait, block until an OS signal arrives, then call `Shutdown` with a deadline.

```go
func main() {
    e := setupRouter()
    e.HideBanner = true

    go func() {
        if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
            e.Logger.Fatal(err)
        }
    }()

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    <-ctx.Done()

    sctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := e.Shutdown(sctx); err != nil {
        e.Logger.Fatal(err)
    }
}
```

*What just happened:* a few small, deliberate pieces. We start `e.Start` in a goroutine so `main` doesn't block on it - note the `err != http.ErrServerClosed` check, because a *clean* shutdown makes `Start` return that exact error and we don't want to treat success as a crash. `signal.NotifyContext` hands 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 fires. Once it does, `e.Shutdown(sctx)` stops accepting new connections and waits for in-flight requests to finish, up to the 5-second deadline set with `context.WithTimeout` - past that it gives up so a stuck request can't block the deploy forever.

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

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

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 books-api .
```

*What just happened:* `CGO_ENABLED=0` disables cgo so the binary doesn't dynamically link against the system C library - fully self-contained, runs on a bare `scratch` or `distroless` image with nothing else installed. `GOOS=linux` cross-compiles for Linux even from a Mac or Windows machine. The output is one file, `books-api`, that you 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
}
e.Start(addr)
```

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

Put that binary in a tiny multi-stage container and stand a **reverse proxy** in front - 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 Echo app speaks plain HTTP on its port; the proxy faces the public internet.

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

## Recap

- An `*echo.Echo` 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 `e.ServeHTTP(rec, req)`, then inspect `rec.Code` and `rec.Body`. No network, no ports.
- Two styles: **`ServeHTTP`** runs the full chain (routing + middleware + error handler), while **`e.NewContext(req, rec)`** calls one handler directly - and since handlers return an `error`, check that return value too.
- Factor route setup into a **`setupRouter()`** that both `main` and tests call, so there's one source of truth and no drift.
- For production, set `e.HideBanner = true` / `e.HidePort = true` to quiet Echo's chatter, and set real `e.Server.ReadTimeout` / `WriteTimeout` (the default is none).
- For a clean exit, start `e.Start` in a goroutine, wait for `SIGINT`/`SIGTERM` via `signal.NotifyContext`, then call `e.Shutdown(ctx)` with a deadline - 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 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 an Echo router with net/http/httptest and no real network?",
    "choices": ["Echo spins up a hidden test server on a random port", "An *echo.Echo 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 - Echo tests always need a running server"],
    "answer": 1,
    "explain": "Because the instance satisfies http.Handler, ServeHTTP(rec, 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": "When testing a single Echo handler directly with e.NewContext, what should you check that you don't with the ServeHTTP style?",
    "choices": ["The TCP connection state", "The error value the handler returns, since a handler is func(c echo.Context) error", "Nothing extra - both styles are identical", "The server's read timeout"],
    "answer": 1,
    "explain": "Calling a handler directly via e.NewContext bypasses routing and the error handler, so the handler's returned error isn't rendered for you. Check both the returned error and rec.Code."
  },
  {
    "q": "During a graceful shutdown, e.Start() returns a specific error. How should you treat it?",
    "choices": ["As a fatal crash - Logger.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 e.Start entirely", "Retry e.Start in a loop"],
    "answer": 1,
    "explain": "e.Shutdown causes e.Start to return http.ErrServerClosed. Check for it explicitly; a blanket Logger.Fatal on any error would make every clean shutdown look like a crash and exit non-zero."
  }
]
```


---

# Where to Go Next

Look at what you can actually do now. You can spin up an Echo server, route requests with
path and query parameters, group routes, bind and validate JSON into structs with `c.Bind`,
shape responses with `c.JSON` and the right status codes, write and chain middleware, build
full CRUD for a resource, and let Echo's central `HTTPErrorHandler` turn returned errors into
clean HTTP responses - then test the whole thing with `httptest` and ship it with graceful
shutdown. That's a real REST API, not a toy.

And here's the quieter win. Echo is a thin, error-clean layer over `net/http`. An
**instance** (`echo.New()`) holds your routes and middleware, an **`echo.Context`** carries
each request, and your handlers `func(c echo.Context) error` *return* their failures for one
central handler to render. Nothing was hidden behind magic - so when something breaks at
2am, you can reason about it.

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

## Echo 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*, not whole different universes.

```mermaid
flowchart TD
  Start[Need a Go web service?] --> Std{Stdlib purity?}
  Std -- Yes, minimal --> Chi[chi or net/http]
  Std -- No, want batteries --> Style{Handler style?}
  Style -- Return an error --> Echo[Echo]
  Style -- Write to a context --> Gin[Gin]
```

A line on each:

- **Echo** - error-returning handlers (`func(c echo.Context) error`), a generous set of
  built-in middleware, and a central `HTTPErrorHandler` that renders failures in one place.
  If you like writing handlers that hand their errors *up* rather than writing the response
  by hand, this is your framework. (You're here.)
- **Gin** - the most popular Go web framework, with the biggest ecosystem and the most Stack
  Overflow answers. The main stylistic difference: handlers take a `*gin.Context` and *write*
  to it instead of returning an error. See [Gin From Zero](/guides/gin-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).
- **The standard library alone** - for many services, `net/http` plus modern Go's routing is
  genuinely enough. Knowing what Echo 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: Echo and Gin are *very* close - same speed class, same batteries-included
> spirit. The real choice between them is taste: do you prefer handlers that **return an error**
> (Echo) or ones that **write to a context** (Gin), and which built-in middleware you want out
> of the box. Reach for **chi or net/http** instead when you want stdlib purity and zero lock-in.

📝 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 *this* team?"

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

Every API in this guide stored books 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 Echo
service grows is a **database**.

Here's the reassuring part: your handlers barely change. Remember how Phase 6 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 (or an error). 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 `Book` struct, point it at SQLite (or Postgres later), and your create/read/
update/delete calls become real persistence. The shape of your Phase 6 handlers 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. Take the **books API** you
grew across this guide and carry it all the way home:

- **Swap the in-memory store for GORM + SQLite** so books survive a restart. The handlers stay;
  the store changes. ([GORM From Zero](/guides/gorm-from-zero) walks the persistence part.)
- **Add JWT auth** with Echo's built-in `middleware.JWT` so each request proves who it is, and
  books 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 - Echo ships a Logger middleware to start from.
- **Generate API docs** with OpenAPI/Swagger 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 the graceful shutdown from Phase 7 wired up.

If the books API feels too familiar, build something small and new end to end instead - a
**notes API** or a **bookmarks API**. Same muscles: routes, binding, a store, middleware, tests,
deploy. Finishing one project completely teaches more than three more tutorials would.

## The clear-eyed close

Echo was never magic. Strip the helpers away and it's a handful of things you now understand
completely: an **instance** that holds your routes, a **context** that carries each request, a
**middleware chain** that wraps it all, and handlers that **return errors** for a central handler
to render - 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, and reason about it when it misbehaves. Go finish the
books API, give it a database, lock it behind JWT, deploy it, and show someone. You're ready.

## Recap

1. **You can ship a real Echo API** - routed, bound and validated, middleware-wrapped, tested,
   and deployed - and you understand *why*, because Echo hid nothing behind magic.
2. **Echo and Gin are close cousins** - same speed class and batteries; pick by handler style
   (Echo returns errors, Gin writes to a context) and which built-in middleware you want.
3. **Reach for chi or net/http** when you want stdlib purity and zero lock-in instead of batteries.
4. **A database is the next layer** - most Echo services add one, and with the Phase 6 separation
   in place your handlers barely change; you swap the in-memory store for GORM.
5. **Build and finish one thing** - carry the books API to GORM + SQLite, JWT auth, request logging,
   OpenAPI docs, real config, and a deploy. Or build a small notes / bookmarks API end to end.

## Quick check

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

```quiz
[
  {
    "q": "You like writing handlers that return an error for one central handler to render, and you want generous built-in middleware. Which framework fits best?",
    "choices": [
      "chi, because it's minimal",
      "Echo, with its error-returning handlers and central HTTPErrorHandler",
      "net/http alone, always",
      "Gin, because it writes to a context"
    ],
    "answer": 1,
    "explain": "Echo's whole personality is handlers that return an error (func(c echo.Context) error) for a central HTTPErrorHandler, plus a generous set of built-in middleware. Gin writes to a context instead; chi and net/http favor stdlib purity over batteries."
  },
  {
    "q": "How should you plainly choose between Echo and Gin?",
    "choices": [
      "Gin is always faster, so pick Gin",
      "They're very close - pick by handler style (return an error vs write to a context) and which built-in middleware you want",
      "Echo is built on fasthttp, so it can't use net/http middleware",
      "Echo is for beginners and Gin is for experts"
    ],
    "answer": 1,
    "explain": "Echo and Gin are in the same speed and batteries class. The real difference is taste: error-returning handlers (Echo) versus writing to a context (Gin), and which built-in middleware each gives you. Both sit on net/http."
  },
  {
    "q": "You're adding a real database to your books API from Phase 6. 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 Echo and switch to chi",
      "Nothing - Echo 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 return a response or error. You swap the store from a map to GORM + a database - the bottom layer changes, the top stays."
  }
]
```
