# actix-web From Zero

> Learn one of the fastest Rust web frameworks: the App and HttpServer, routing and extractors, responders, shared state with web::Data, middleware, a full REST API with the ResponseError trait, and testing and production. Mature, batteries-included, and a perennial benchmark leader.


---

# actix-web From Zero

actix-web is one of the oldest, fastest, and most battle-tested web frameworks in Rust - it sits at or
near the top of the TechEmpower benchmarks year after year, and a lot of production Rust runs on it. Where
[axum](/guides/axum-from-zero) is the newer, tower-native option, actix-web is the mature, feature-packed
one: routing, extractors, middleware, websockets, and more, all included. The name carries a bit of
history - it grew out of an actor framework - but day to day you write plain `async fn` handlers and rarely
touch actors at all.

The mental model is three pieces. An **`App`** holds your routes and shared state; an **`HttpServer`**
runs one or more copies of that `App` across worker threads; and a **handler** is an `async fn` whose
arguments are **extractors** (`Path`, `Query`, `Json`, `web::Data`) and whose return value is a
**`Responder`**. If that sounds like axum, it is - the big Rust frameworks converged on "extract from the
request, return something that becomes a response." The differences are in the details, and this guide
walks them.

> 📝 This teaches the **framework** - it assumes you know **Rust** (ownership, traits, `Result`,
> `async`/`await` - [Rust From Zero](/guides/rust-from-zero)). It's most useful read alongside
> [axum](/guides/axum-from-zero) (the closest comparison) and on top of [Tokio](/guides/tokio-the-async-runtime).
> actix-web compiles and runs as a Rust program, so examples are shown with the commands to run them.

## How to read this

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

## The phases

**Part 1 - The core (🟢 Basic)**
1. **[What actix-web Is & Your First Server](01-what-actix-web-is.md)** 🟢 - `App`, `HttpServer`, the async handler, and a running server.
2. **[Routing & Extractors](02-routing-and-extractors.md)** 🟢 - methods, scopes, `Path`/`Query`/`Json`, and how extractors work.
3. **[Responders](03-responders.md)** 🟡 - `HttpResponse`, `impl Responder`, returning JSON and status codes.

**Part 2 - A real API (🟡 → 🔴)**
4. **[Shared State with web::Data](04-shared-state.md)** 🟡 - `web::Data<T>`, `app_data`, and sharing a pool across workers.
5. **[Middleware](05-middleware.md)** 🔴 - `wrap`, the built-in `Logger`, `from_fn`, and writing your own.
6. **[A REST API with Error Handling](06-rest-api-and-errors.md)** 🔴 - full CRUD plus the `ResponseError` trait for clean error responses.

**Part 3 - Ship it (🟡 → 🟢)**
7. **[Testing & Production](07-testing-and-production.md)** 🟡 - `actix_web::test`, workers, and deployment.
8. **[Where to Go Next](08-where-to-go-next.md)** 🟢 - actix-web vs axum/Rocket, data layers, and what to build.

> The throughline: an **`App`** of routes, run by an **`HttpServer`**, with handlers that **extract from
> the request and return a `Responder`**. Mature, fast, and more familiar than its actor heritage suggests.


---

# What actix-web Is & Your First Server

You know [Rust](/guides/rust-from-zero) and want something fast and production-proven on the web. That's
actix-web's corner: one of the oldest Rust frameworks, a perennial TechEmpower top finisher, and the
mature, batteries-included sibling to [axum](/guides/axum-from-zero) - routing, extractors, middleware,
websockets, and JSON all in the box.

The name comes from an **actor** framework it grew out of, but day to day you write plain `async fn`
handlers and never touch an actor - the heritage powers internals, not your code. (If "web framework"
itself is fuzzy, [What a Framework Even Is](/guides/what-a-framework-even-is) covers the ground first.)

> 📝 This guide teaches the **framework**, not the language. It assumes you're comfortable with Rust - 
> ownership, traits, `Result`, `async`/`await`. actix-web compiles and runs as a normal Rust program, so
> examples come with the commands to build and run them.

## The mental model: three pieces

Almost everything in actix-web hangs off three nouns.

📝 An **`App`** holds your **routes** (and later, shared state). You build it by chaining `.route(...)`
calls - the blueprint of your service: "a `GET /ping` goes here, a `POST /articles` goes there."

📝 An **`HttpServer`** **runs** copies of that `App`. It opens the socket, accepts connections, and spreads
the work across **worker threads** - and here's the twist that trips people up later: it builds a *separate
`App` per worker*. More on that in a moment.

📝 A **handler** is an **`async fn`** whose return value becomes the response, because it returns something
that implements the **`Responder`** trait. That's actix-web's version of axum's `IntoResponse` - the return
type *is* the response.

Say it once so it sticks: **an `App` holds routes, an `HttpServer` runs copies of the `App` across workers,
and each handler is an `async fn` returning a `Responder`.** That sentence is the spine of every actix-web
service you'll ever write.

```mermaid
flowchart LR
  S[HttpServer<br/>runs + spreads load] --> A1["App (worker 1)<br/>holds routes"]
  S --> A2["App (worker 2)<br/>holds routes"]
  A1 --> H["handler<br/>async fn ping()"]
  H --> R["Responder<br/>becomes the response"]
```

*One idea:* the server runs many copies of your app at once, and a request flows into one of them, hits the
matching handler, and the handler's return value flows back out as the response.

## Your first server

First, add the dependency. From inside your Cargo project:

```bash
cargo add actix-web
```

*What just happened:* `cargo add actix-web` pulls in the framework and writes it into your `Cargo.toml`
under `[dependencies]`. Unlike axum, there's no separate `cargo add tokio` - actix-web ships its own
runtime (built on Tokio) and re-exports the macro you need. One crate, and you're ready.

Now the smallest server that does something real. Put this in `src/main.rs`:

```rust
use actix_web::{web, App, HttpServer, Responder, HttpResponse};

async fn ping() -> impl Responder {
    HttpResponse::Ok().body("pong")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().route("/ping", web::get().to(ping))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:*
- `async fn ping() -> impl Responder` is the **handler**. It returns `HttpResponse::Ok().body("pong")` - a
  `200 OK` with body `pong`. `impl Responder` means "some type that knows how to become a response"; Phase 3
  covers the rest.
- `#[actix_web::main]` rewrites `async fn main` to start actix-web's runtime - the counterpart to axum's
  `#[tokio::main]`. Rust's real `main` can't be `async` on its own.
- `HttpServer::new(|| { ... })` takes a **closure that builds an `App`**. `App::new().route("/ping",
  web::get().to(ping))` registers one route: `GET /ping` runs `ping`. Read it as "for GET, call `ping`" - 
  there's also `web::post()`, `web::put()`, `web::delete()`.
- `.bind(("127.0.0.1", 8080))?` opens the socket; binding can fail, so `?` propagates the error, which is
  why `main` returns `std::io::Result<()>`.
- `.run().await` starts the accept loop and blocks, handing each request to a worker's `App`.

Build and run it like any Rust binary:

```bash
cargo run
```

actix-web prints a couple of startup lines and then waits for requests. Leave it running, and in another
terminal hit the route:

```bash
curl 127.0.0.1:8080/ping
```

```console
$ curl 127.0.0.1:8080/ping
pong
```

*What just happened:* `curl` sent a `GET /ping`. The server routed it into one of its worker `App`s, matched
the route to your `ping` handler, called it, and the returned `HttpResponse` came back as the response body
 - `pong`. A working, multi-threaded HTTP server in about a dozen lines.

## The catch worth flagging now: the closure runs *per worker*

Look again at `HttpServer::new(|| { App::new()... })`. That closure isn't called once. actix-web spawns
multiple **worker threads** (by default, one per CPU core) and calls your closure **once on each worker**
to build that worker's own `App`. You end up with several independent `App`s running side by side.

⚠️ Fine for the toy above - building a couple of routes a few times costs nothing. But the moment you want
**shared state** (a database pool, a cache, a counter), creating it *inside* the closure backfires: each
worker gets its own separate copy, and they never see each other's data. That's why state in actix-web goes
through a special wrapper instead - file away the shape of the problem for now.
[Phase 4: Shared State with web::Data](04-shared-state.md) solves it properly with `web::Data<T>`.

## The running example: an articles API

We won't keep returning `pong`. Across this guide we'll grow one real service - a small **articles API** - 
and the core of it is a single type: an article with an id, a title, and a body.

```rust
struct Article {
    id: u32,
    title: String,
    body: String,
}
```

*What just happened:* the `Article` struct is a plain type for now - three fields, no traits. Phase 3
derives `Serialize`/`Deserialize` on it so actix-web can turn it into JSON and parse it from a request
body, the moment it becomes a real API resource.

Next: routing in earnest - multiple methods, scopes for grouping routes, and the extractors (`Path`,
`Query`, `Json`) that pull pieces out of the incoming request.

## Recap

- **actix-web** is the mature, fast, batteries-included Rust framework - a perennial TechEmpower leader and
  the closest peer to [axum](/guides/axum-from-zero). Its **actor** heritage powers internals, not your code.
- **Mental model:** an **`App`** holds routes, an **`HttpServer`** runs copies of it across worker threads,
  and a **handler** is an `async fn` returning a **`Responder`**.
- **First server:** `cargo add actix-web` (bundles its own runtime, no separate Tokio install), build the
  `App` inside `HttpServer::new(|| ...)`, register `web::get().to(handler)`, then `.bind(...)?.run().await`.
  `#[actix_web::main]` lets `main` be async.
- **`HttpServer::new`'s closure runs once per worker** - each worker builds its own `App`, so shared state
  can't live inside it. `web::Data` fixes that in Phase 4.
- **Run with `cargo run`, test with `curl`** - the handler's `HttpResponse` is exactly what the client gets.
- **Throughline:** an `App` of routes, run by an `HttpServer`, handlers returning a `Responder` - grown into
  one **articles API** across the guide.

## Quick check

Three questions on the ideas that have to stick - the three-piece model, what the handler returns, and the
per-worker closure:

```quiz
[
  {
    "q": "In actix-web's mental model, what is the relationship between App and HttpServer?",
    "choices": [
      "App holds the routes; HttpServer runs copies of that App across worker threads",
      "HttpServer holds the routes; App runs them on a single thread",
      "They are the same type with two names",
      "App is the database and HttpServer is the cache"
    ],
    "answer": 0,
    "explain": "An App is the blueprint of routes (and later state). An HttpServer opens the socket and runs copies of that App across worker threads, handing each request to one of them."
  },
  {
    "q": "What does the `ping` handler's return value become, and why is its type `impl Responder`?",
    "choices": [
      "The HTTP response, because any type implementing Responder knows how to become one",
      "A log line printed to the server console",
      "An argument passed to the next handler",
      "Nothing - you must call a separate send() function"
    ],
    "answer": 0,
    "explain": "A handler's return value is the response. `impl Responder` means 'some type that implements the Responder trait', so actix-web knows how to turn it into a full HTTP response. HttpResponse is one such type."
  },
  {
    "q": "Why does `HttpServer::new` take a closure rather than a single built App?",
    "choices": [
      "Because the closure is called once per worker thread, so each worker builds its own App",
      "Because closures are faster to compile than structs",
      "Because the App must be rebuilt on every incoming request",
      "Because Rust forbids passing a struct to a function"
    ],
    "answer": 0,
    "explain": "actix-web spawns multiple workers and calls the closure once on each to build that worker's own App. That's why shared state can't live inside the closure - each worker would get a separate copy. Phase 4's web::Data solves it."
  }
]
```


---

# Routing & Extractors

Phase 1 stood up an `App`, an `HttpServer`, and one handler answering one path. This phase covers
how actix-web decides *which* handler runs for an incoming request, and how that handler gets the
request pieces it needs without ever touching the raw bytes.

Two mental models cover everything else.

> 📝 **Mental model #1 - a route is a tiny rule: `method + path → handler`.** When a request
> arrives, actix-web walks its list of registered rules looking for the first one whose HTTP
> method *and* path pattern match. `GET /articles` and `POST /articles` are two different routes
> even though the path is identical, because the method is part of the key.

> 📝 **Mental model #2 - a handler's parameters are extractors.** You don't reach into the request
> object. Instead you declare what you want as typed arguments - `web::Path<u32>`,
> `web::Query<Pagination>`, `web::Json<NewArticle>` - and actix-web *extracts* those values from
> the request before your function body ever runs. If extraction fails (bad path segment,
> malformed JSON), your handler isn't called at all; the framework returns an error response for
> you. Your function body only ever sees already-valid, already-typed data.

We'll keep growing the **articles API** from Phase 1 - same familiar shape:

```rust
struct Article {
    id: u32,
    title: String,
    body: String,
}
```

## Registering routes: two styles, same idea

actix-web gives you two ways to attach a handler to a route, producing identical behavior - the
difference is purely *where the route lives in your source*. You'll see both in the wild.

### Style 1: the builder

You spell out the route on the `App` itself with `.route(path, method().to(handler))`:

```rust
use actix_web::{web, App, HttpServer, HttpResponse, Responder};

async fn list() -> impl Responder {
    HttpResponse::Ok().body("all articles")
}

async fn show() -> impl Responder {
    HttpResponse::Ok().body("one article")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/articles", web::get().to(list))
            .route("/articles/{id}", web::get().to(show))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `web::get()` builds a route guard that only matches `GET` requests; `.to(list)`
says "when it matches, call `list`." Chaining two `.route(...)` calls gives the routing table two
rules. The `{id}` in the second path is a **placeholder** - it matches any single path segment
(`/articles/7`, `/articles/42`) and captures it for an extractor to read later. Routes are checked
top to bottom, and the first match wins.

### Style 2: attribute macros

The same routes, but the method and path move *up onto the handler* as an attribute, and you
register the handler with `.service(...)`:

```rust
use actix_web::{get, App, HttpServer, HttpResponse, Responder};

#[get("/articles")]
async fn list() -> impl Responder {
    HttpResponse::Ok().body("all articles")
}

#[get("/articles/{id}")]
async fn show() -> impl Responder {
    HttpResponse::Ok().body("one article")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(list)
            .service(show)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `#[get("/articles")]` bundles the method + path *with* the function. The
function now *is* a service, registered with `.service(list)` instead of a `.route(...)` call.
There's a macro for each verb - `#[post(...)]`, `#[put(...)]`, `#[delete(...)]`, and so on.
Behaviorally this is the same routing table as Style 1; many codebases prefer it because each
handler carries its own route declaration right above it instead of a central list elsewhere.

> 💡 Pick one style and stay consistent within a project - a reader shouldn't have to check two
> places to learn where routes are defined. Macros read nicely for CRUD-style apps; the builder
> shines when composing routes programmatically.

## Grouping routes with `web::scope`

Real APIs version their endpoints and share common prefixes - `/api/v1/articles`,
`/api/v1/authors`, and so on. Typing `/api/v1/...` in front of every route is noisy and error-prone.
`web::scope` mounts a whole group of routes under a shared prefix:

```rust
use actix_web::{web, App, HttpServer, HttpResponse, Responder};

async fn list() -> impl Responder {
    HttpResponse::Ok().body("all articles")
}

async fn show() -> impl Responder {
    HttpResponse::Ok().body("one article")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            web::scope("/api/v1")
                .route("/articles", web::get().to(list))
                .route("/articles/{id}", web::get().to(show)),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `web::scope("/api/v1")` creates a sub-router whose prefix is `/api/v1`. Every
route registered inside it is *relative* to that prefix, so `/articles` actually answers
`GET /api/v1/articles` and `/articles/{id}` answers `GET /api/v1/articles/7`. To cut a v2 of the
API, add a second scope and leave v1 untouched. Scopes can also carry their own state and
middleware (Phases 4 and 5).

## Extractors: turning a request into typed arguments

A handler's parameters are how it asks for pieces of the request. The three you'll reach for
constantly are `Path`, `Query`, and `Json`.

### `web::Path` - values from the URL

That `{id}` placeholder captures a path segment. `web::Path<T>` pulls it out and parses it into the
type you ask for:

```rust
use actix_web::{web, HttpResponse, Responder};

async fn show(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().body(format!("you asked for article {id}"))
}
```

*What just happened:* the handler declared `path: web::Path<u32>`. actix-web took the `{id}` segment
from the URL, parsed it as a `u32`, and only then called `show`. `path.into_inner()` unwraps the
`Path` wrapper to give you the plain `u32` inside. If the URL had been `/articles/banana`, the parse
would fail, `show` would never run, and the client would get a `400 Bad Request` automatically - zero
validation code needed for "is this segment actually a number."

When a route has several placeholders, ask for a tuple and destructure it:

```rust
use actix_web::{web, HttpResponse, Responder};

// route registered as "/authors/{name}/articles/{id}"
async fn show_by_author(path: web::Path<(String, u32)>) -> impl Responder {
    let (name, id) = path.into_inner();
    HttpResponse::Ok().body(format!("article {id} by {name}"))
}
```

*What just happened:* with two placeholders, `web::Path<(String, u32)>` captures both in order - 
`{name}` becomes the `String`, `{id}` becomes the `u32`. `into_inner()` hands back the tuple to
destructure in one line. The tuple's *order* matches the order the placeholders appear in the path,
not their names, so keep them lined up.

### `web::Query` - values from the query string

Query parameters (`?page=2&per_page=20`) come in through `web::Query<T>`, where `T` is a struct that
derives serde's `Deserialize`:

```rust
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: u32,
    per_page: u32,
}

async fn list(q: web::Query<Pagination>) -> impl Responder {
    HttpResponse::Ok().body(format!("page {} of {} per page", q.page, q.per_page))
}
```

*What just happened:* `#[derive(Deserialize)]` on `Pagination` tells serde how to build it from
key/value pairs. `web::Query<Pagination>` reads the query string, matches each field by name, and
parses the values into the field types. A request to `/articles?page=2&per_page=20` gives you
`q.page == 2` and `q.per_page == 20`. As with `Path`, a missing or unparseable field means the
handler is never called and the client gets a `400`. (Add `serde` to `Cargo.toml` with
`features = ["derive"]`; we lean on it heavily from here on.)

### `web::Json` - the request body

For `POST`/`PUT` bodies sent as JSON, `web::Json<T>` deserializes the body into your type:

```rust
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct NewArticle {
    title: String,
    body: String,
}

async fn create(body: web::Json<NewArticle>) -> impl Responder {
    let article = body.into_inner();
    HttpResponse::Ok().body(format!("creating '{}'", article.title))
}
```

*What just happened:* `web::Json<NewArticle>` read the raw request body, parsed it as JSON, and
deserialized it into a `NewArticle` before `create` ran. `body.into_inner()` (or field access like
`body.title`) gets at the data. A malformed body or missing required field produces a `400`
automatically - your handler only ever sees a fully-formed `NewArticle`.

## Combining extractors - and the one body rule

Extractors compose: a handler can ask for several at once, and actix-web fills them all in before
calling you. A create-under-an-author handler might want the author from the path *and* the article
from the body:

```rust
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct NewArticle {
    title: String,
    body: String,
}

// route: "/authors/{name}/articles"
async fn create_for_author(
    path: web::Path<String>,
    body: web::Json<NewArticle>,
) -> impl Responder {
    let author = path.into_inner();
    let article = body.into_inner();
    HttpResponse::Ok().body(format!("'{}' by {author}", article.title))
}
```

*What just happened:* the handler declared two extractors as separate parameters. actix-web ran both
 - pulled `{name}` from the URL into `path`, deserialized the JSON body into `body` - and only then
called `create_for_author`. List as many extractors as you need; they're just function parameters.

> ⚠️ One real constraint: **only one extractor may read the request body.** `web::Json` consumes
> the body stream, and a body can only be read once. A handler can have many `Path` and `Query`
> extractors but effectively **one** body extractor (`Json`, `Form`, or `Bytes`) - asking for two
> won't give you the data twice, it's a design error. More on `Json` in
> [Phase 3: Responders](03-responders.md).

## Recap

- A route is `method + path → handler`. `GET /articles` and `POST /articles` are distinct routes.
- Register routes two ways: the **builder** (`.route("/articles", web::get().to(list))`) or
  **attribute macros** (`#[get("/articles")]` + `.service(list)`) - same behavior, pick one and stay
  consistent.
- `web::scope("/api/v1")` mounts a group of routes under a shared prefix, for versioning an API.
- Extractors turn a request into typed arguments: `web::Path` (URL segments, `.into_inner()`),
  `web::Query` (query string into a `Deserialize` struct), `web::Json` (the request body).
- Failed extraction means your handler never runs - the client gets a `400` automatically.
- You can combine many extractors as parameters, but only **one** may read the body.

## Quick check

```quiz
[
  {
    "q": "What two things together make up a route in actix-web?",
    "choices": ["The path and the handler's return type", "The HTTP method and the path pattern", "The query string and the body", "The scope prefix and the port"],
    "answer": 1,
    "explain": "actix-web matches an incoming request by both its HTTP method and its path pattern, which is why GET /articles and POST /articles are different routes."
  },
  {
    "q": "Which extractor reads values out of the URL path, like the 7 in /articles/7?",
    "choices": ["web::Query", "web::Json", "web::Path", "web::Data"],
    "answer": 2,
    "explain": "web::Path<T> captures the {id} placeholder from the path and parses it into T; you unwrap it with .into_inner()."
  },
  {
    "q": "How many extractors in a single handler may read the request body?",
    "choices": ["As many as you want", "Exactly one", "Two, one for JSON and one for form data", "Zero - the body is never extracted"],
    "answer": 1,
    "explain": "The body stream can only be read once, so a handler can have many Path/Query extractors but effectively only one body extractor such as web::Json."
  }
]
```


---

# Responders

The whole chapter in one sentence: **a handler returns a value, and actix-web only accepts it if its type implements `Responder`.** You don't call that trait yourself - you return a type that implements it, and the framework does the conversion.

The question for every handler isn't "how do I write a response?" but "what type do I return?" Pick the right one for the job:

- Need full control over status, headers, and body? Return **`HttpResponse`** - the workhorse.
- Just shipping some JSON with a 200? Return **`web::Json<T>`** and let it serialize for you.
- Returning one consistent shape and want to stay terse? Return **`impl Responder`**.

> 📝 [Phase 2](02-routing-and-extractors.md) covered the *input* half of a handler - extractors pull `Path`, `Query`, and `Json` out of the request. This chapter is the *output* half. Input by extractor, output by `Responder`: that symmetry is the heart of how actix handlers are shaped.

Throughout we'll keep growing the **articles API**. Here's the model we're returning:

```rust
use serde::Serialize;

#[derive(Serialize)]
struct Article {
    id: u32,
    title: String,
    body: String,
}
```

*What just happened:* `#[derive(Serialize)]` from `serde` is the one prerequisite for sending a struct as JSON - without it, none of the `.json()` calls below would compile. Everything here assumes that derive is present.

## HttpResponse: the workhorse

`HttpResponse` is a builder. Start with a status from a named method (`Ok()`, `Created()`, `NotFound()`, and friends), then finish it one of three ways: `.json(&value)` to serialize a body as JSON, `.body("…")` for a raw body, or `.finish()` for no body at all.

```rust
use actix_web::{get, HttpResponse, Responder};

#[get("/articles/{id}")]
async fn get_article() -> impl Responder {
    let article = Article {
        id: 1,
        title: "Hello, actix".to_string(),
        body: "An article from our API.".to_string(),
    };

    HttpResponse::Ok().json(&article)
}
```

*What just happened:* `HttpResponse::Ok()` starts a `200 OK` response, and `.json(&article)` serializes the struct into the body and sets `Content-Type: application/json` for you. The return type is `impl Responder` because `HttpResponse` implements `Responder` - read it as "returns *something that can become a response*."

Returning a list is the same move - serde serializes a `Vec<Article>` into a JSON array:

```rust
use actix_web::{get, HttpResponse, Responder};

#[get("/articles")]
async fn list_articles() -> impl Responder {
    let articles = vec![
        Article { id: 1, title: "First".to_string(), body: "…".to_string() },
        Article { id: 2, title: "Second".to_string(), body: "…".to_string() },
    ];

    HttpResponse::Ok().json(&articles)
}
```

*What just happened:* `.json()` serializes *anything* that's `Serialize`, including a `Vec` - a list of articles becomes a JSON array with no extra ceremony.

`HttpResponse` earns "workhorse" status from its other status methods. Each is a different status code, pairing naturally with `.json()`, `.body()`, or `.finish()`:

```rust
use actix_web::HttpResponse;

// 201 Created - return the thing you just made.
HttpResponse::Created().json(&article);

// 204 No Content - success, nothing to send back (e.g. a DELETE).
HttpResponse::NoContent().finish();

// 404 Not Found - no body needed.
HttpResponse::NotFound().finish();

// 400 Bad Request - a plain-text explanation.
HttpResponse::BadRequest().body("id must be a positive integer");
```

*What just happened:* each builder picks a status; the finisher decides the body. Use `.json()` for a serializable value, `.body()` for plain text or raw bytes, `.finish()` when the status *is* the whole message. Named helpers cover the common codes; for anything exotic, `HttpResponse::build(StatusCode::IM_A_TEAPOT)` builds from a raw status.

> 💡 A useful instinct: the moment you want to control the status code, that's your cue to use `HttpResponse`. The other return types are conveniences that pin the status for you - great until you need to say `201` or `404`.

## web::Json: the shorthand

If your handler always returns a 200 with a JSON body, `HttpResponse::Ok().json(…)` is a touch verbose. `web::Json` is the shortcut: wrap your value in `web::Json(...)`, return it, and actix serializes it as a 200 automatically.

```rust
use actix_web::{get, web, Responder};

#[get("/articles/latest")]
async fn latest_article() -> impl Responder {
    let article = Article {
        id: 7,
        title: "Latest".to_string(),
        body: "The newest article.".to_string(),
    };

    web::Json(article)
}
```

*What just happened:* `web::Json(article)` is a responder that serializes its inner value and responds with `200 OK` - the same as `HttpResponse::Ok().json(&article)`, with less typing. You hand it the value by ownership, since the wrapper takes it over.

You met `web::Json` in [Phase 2](02-routing-and-extractors.md) as an *extractor* pulling a JSON body out of the request. Same type, both directions: input as a parameter, output as a return value.

> ⚠️ The tradeoff is real: `web::Json` *always* responds with 200. Need a `201 Created` after a POST, or a `404` when the article doesn't exist? `web::Json` can't express it - go back to `HttpResponse::Ok().json(...)` / `HttpResponse::Created().json(...)` to choose the status. Reach for `web::Json` on read paths where 200 is genuinely always correct; use `HttpResponse` everywhere the status varies.

## The trap: different branches, different types

This one bites everyone exactly once. Write a handler that returns 200 when it finds the article and 404 when it doesn't, and the compiler refuses to build it:

```rust
use actix_web::{get, web, HttpResponse, Responder};

// ⚠️ This does NOT compile.
#[get("/articles/{id}")]
async fn get_article(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();

    if id == 0 {
        HttpResponse::NotFound().finish()   // one type…
    } else {
        web::Json(Article {                 // …a DIFFERENT type
            id,
            title: "Found".to_string(),
            body: "…".to_string(),
        })
    }
}
```

*What just happened:* the two branches return *different concrete types* - one `HttpResponse`, the other `web::Json<Article>`. `impl Responder` means "some single type that implements `Responder`," and a function can only return one concrete type, even if both implement it. The compiler error, "expected `HttpResponse`, found `Json<Article>`," is its way of saying "pick one type."

The fix: make every branch produce the *same* concrete type. Easiest choice is `HttpResponse` for both, since it can represent any status:

```rust
use actix_web::{get, web, HttpResponse, Responder};

#[get("/articles/{id}")]
async fn get_article(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();

    if id == 0 {
        HttpResponse::NotFound().finish()
    } else {
        HttpResponse::Ok().json(&Article {
            id,
            title: "Found".to_string(),
            body: "…".to_string(),
        })
    }
}
```

*What just happened:* both branches now return `HttpResponse`, so the function has one consistent return type and the compiler is happy. Status and body differ, but the type is identical, and that's all Rust cares about.

> 💡 Rule of thumb: **the moment a handler can return more than one status, return `HttpResponse` from every branch.** Save `web::Json` and bare `impl Responder` for handlers with exactly one outcome shape. (A cleaner way to vary status exists - returning a `Result` and letting `ResponseError` map errors to status codes, covered in [Phase 6](06-rest-api-and-errors.md). For now, one `HttpResponse` type in branchy handlers is the working answer.)

## impl Responder vs HttpResponse: which to write

`impl Responder` in the return position means "I'm returning *some* type that implements `Responder`, and I'd rather not spell out which." It's ergonomic when there's a single, obvious response shape - a handler that always returns a `web::Json<Article>`, or always an `HttpResponse`.

`HttpResponse` is the explicit, flexible choice: write it when you need control over the status, when branches must agree on a type (the trap above), or when the signature should state plainly "this returns an HTTP response."

```rust
use actix_web::{get, web, HttpResponse, Responder};

// Single shape, terse: impl Responder is a fine fit.
#[get("/ping")]
async fn ping() -> impl Responder {
    web::Json(serde_json::json!({ "status": "ok" }))
}

// Status varies / branches: be explicit with HttpResponse.
#[get("/articles/{id}/exists")]
async fn exists(path: web::Path<u32>) -> HttpResponse {
    if path.into_inner() == 0 {
        HttpResponse::NotFound().finish()
    } else {
        HttpResponse::Ok().finish()
    }
}
```

*What just happened:* the first handler has one outcome, so `impl Responder` keeps it clean. The second can return two statuses, so it names `HttpResponse` outright, and both branches line up with no fuss. The difference is flexibility (name `HttpResponse`) versus brevity (`impl Responder` for a single shape).

> 📝 Strings work too: a `&'static str` or `String` sends a `200 OK` text body - handy for a health check, rarely what you want for a real API. The articles API speaks JSON, so `HttpResponse` and `web::Json` are your day-to-day tools.

## Recap

- A handler's return type must implement **`Responder`**; the framework calls into that trait to turn your value into an HTTP response.
- **`HttpResponse`** is the workhorse: a builder with status helpers (`Ok`, `Created`, `NotFound`, `NoContent`, `BadRequest`) finished by `.json(&value)`, `.body("…")`, or `.finish()`.
- **`web::Json(value)`** is the shorthand for "200 + JSON body" - terse, but locked to status 200. Same wrapper, extractor on input, responder on output.
- **Different branches must return the same concrete type.** Mixing `HttpResponse` and `web::Json` across `if` branches won't compile; use one `HttpResponse` type everywhere a handler can vary status (or use [Phase 6](06-rest-api-and-errors.md)'s `Result` / `ResponseError`).
- Choose **`impl Responder`** for single-shape, terse handlers; **`HttpResponse`** for control and branchy handlers.

## Quick check

```quiz
[
  {
    "q": "A handler needs to return 200 with an article on success and 404 when it's missing. What return type keeps both branches compiling cleanly?",
    "choices": ["web::Json from one branch, HttpResponse from the other", "HttpResponse from both branches", "impl Responder with the two different wrapper types", "String from both branches"],
    "answer": 1,
    "explain": "Both branches must produce the same concrete type. HttpResponse can represent any status, so returning it from every branch compiles and lets you send 200 or 404."
  },
  {
    "q": "What status does returning web::Json(value) produce?",
    "choices": ["Whatever you set with .status()", "201 Created", "200 OK, always", "204 No Content"],
    "answer": 2,
    "explain": "web::Json is the shorthand for a 200 OK JSON response. To choose a different status you must switch to HttpResponse::Ok().json(...) / HttpResponse::Created().json(...)."
  },
  {
    "q": "You want a 204 No Content response after a successful delete. Which finisher fits?",
    "choices": ["HttpResponse::NoContent().json(&article)", "HttpResponse::NoContent().finish()", "web::Json(())", "HttpResponse::NoContent().body(\"deleted\")"],
    "answer": 1,
    "explain": "A 204 carries no body, so .finish() is the right finisher - it sends the status with no payload. .json() and .body() would attach a body the status says shouldn't exist."
  }
]
```


---

# Shared State with web::Data

So far our articles API has been stateless - each handler builds its response from the request and nothing
else. Real services need *shared* things: a database connection pool, a cache, a config struct, a counter.
The question that trips everyone up isn't "how do I store it" but "how do I make sure every request, on
every worker thread, sees the *same* thing." That's this phase.

## The mental model

In actix-web, a shared dependency lives in **`web::Data<T>`**. You register it on the `App` with
`.app_data(...)`, and any handler that wants it lists `web::Data<T>` as an argument - extraction is **by
type**. The framework looks up the registered value whose type matches and hands it over.

> 💡 `web::Data<T>` is an **`Arc<T>`** under the hood. An `Arc` is a thread-safe, reference-counted pointer:
> cloning it doesn't copy the `T`, it just bumps a counter and hands back another pointer to the *same* `T`.
> We lean on that property hard below.

The subtlety to hold from the start: **`HttpServer` doesn't run one copy of your `App` - it runs one per
worker thread.** The closure you pass to `HttpServer::new` runs *once per worker*. Where you create your
state relative to that closure decides whether your workers share one brain or each get a private one.

## ⚠️ The per-worker closure trap

This is the single most common actix-web state bug, so let's name it precisely. As flagged in
[Phase 1](01-what-actix-web-is.md), the closure passed to `HttpServer::new(...)` is the **app factory**, and
actix-web calls it once on *each* worker thread to build that worker's own `App`. So this looks fine and is
quietly broken:

```rust
// ⚠️ BROKEN: state is built INSIDE the closure
HttpServer::new(|| {
    let state = web::Data::new(AppState {
        articles: Mutex::new(HashMap::<u32, Article>::new()),
    });
    App::new()
        .app_data(state)
        .route("/articles", web::get().to(list))
})
```

*What just happened:* because `web::Data::new(...)` sits *inside* the factory closure, every worker runs it
and gets a **separate, fresh** `HashMap`. Write an article on the thread serving worker A, then read on
worker B, and it's gone. With four workers you effectively have four independent databases, and which one
you hit depends on which thread the OS scheduled your request onto. It'll even look like it works in tests
with a single worker.

The fix is to build the `web::Data` **once, outside** the closure, then `move` it in and `.clone()` it into
each `App`. Cloning a `web::Data` clones the inner `Arc` - so all workers point at the *same* state:

```rust
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use std::collections::HashMap;
use std::sync::Mutex;

struct AppState {
    articles: Mutex<HashMap<u32, Article>>,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // built ONCE, before any worker exists
    let state = web::Data::new(AppState {
        articles: Mutex::new(HashMap::<u32, Article>::new()),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())            // share the SAME state across workers
            .route("/articles", web::get().to(list))
            .route("/articles", web::post().to(create))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `state` is created before `HttpServer::new`, so there's exactly one `Arc<AppState>`.
The closure is now `move`, capturing `state` by value; on each worker it calls `state.clone()`, and an `Arc`
clone is just another handle to that one `AppState`. Four workers, one `HashMap`. The `move` keyword is
load-bearing - without it the closure can't capture an owned value to clone from.

## Reading and writing the state

To get at the state, list `web::Data<AppState>` as a handler argument and actix-web extracts it by type:

```rust
async fn list(state: web::Data<AppState>) -> impl Responder {
    let map = state.articles.lock().unwrap();
    let articles: Vec<&Article> = map.values().collect();
    HttpResponse::Ok().json(articles)
}

async fn create(
    state: web::Data<AppState>,
    body: web::Json<Article>,
) -> impl Responder {
    let mut map = state.articles.lock().unwrap();
    let article = body.into_inner();
    map.insert(article.id, article);
    HttpResponse::Created().finish()
}
```

*What just happened:* `list` locks the `Mutex`, borrows the map read-only, and serializes the values to JSON.
`create` locks it `mut` and inserts. The `web::Json<Article>` extractor pulling the request body is
extraction-by-type again, sitting right next to the state extractor. Both handlers reach through the *same*
`Arc` to the *same* map - exactly what the previous section bought us.

> ⚠️ Why the `Mutex`? `web::Data<T>` is an `Arc<T>`, and an `Arc` only ever gives you a **shared** (`&T`)
> reference - never `&mut T`. `T` itself has to allow mutation through a shared reference, which is what
> **interior mutability** types like `Mutex<...>` (or `RwLock<...>` when reads vastly outnumber writes)
> provide. Forget the `Mutex` and the compiler won't let you write to the map.

### For a real database, you don't need the Mutex

The in-memory `HashMap` is a teaching prop. In a real service your shared state is usually a connection pool
 - and a pool like `sqlx::Pool` is **already** internally synchronized and cheaply cloneable (`Arc`-backed
itself). Wrap it in `web::Data` and use it directly, no `Mutex` in sight:

```rust
struct AppState {
    db: sqlx::PgPool,   // already cloneable + thread-safe; no Mutex needed
}

async fn list(state: web::Data<AppState>) -> impl Responder {
    let rows = sqlx::query_as::<_, Article>("SELECT id, title FROM articles")
        .fetch_all(&state.db)
        .await
        .unwrap();
    HttpResponse::Ok().json(rows)
}
```

*What just happened:* the pool manages its own concurrency, so handlers borrow `&state.db` and run queries
concurrently without any explicit locking. The pool hands out connections, waits when they're all busy, and
returns them after - all thread-safe by design. You still build the pool **once, outside** the closure and
clone the `web::Data` in, exactly as before; the per-worker trap is identical whether your state is a
`HashMap` or a `PgPool`.

## ⚠️ "App data is not configured"

One more sharp edge: if a handler extracts `web::Data<T>` for a type you never registered with
`.app_data(...)`, there's nothing to look up, and actix-web **panics at runtime** with a message like:

```
App data is not configured, to configure use App::app_data()
```

*What just happened:* extraction-by-type means the framework matches your handler's `web::Data<AppState>`
against the registered data by *type*. Register a `web::Data<AppState>` but ask for `web::Data<PgPool>` (or
forget `.app_data` entirely) and the lookup fails. This isn't a compile error - the types are individually
valid - so it surfaces as a 500 and a panic in the logs on the first request that hits that handler. The fix
is almost always "register the exact type the handler asks for."

## Recap

- Shared dependencies live in **`web::Data<T>`**, registered with `.app_data(...)` and extracted by **type**
  as a handler argument.
- `web::Data<T>` is an **`Arc<T>`**; cloning it shares one underlying value rather than copying it.
- ⚠️ The closure passed to `HttpServer::new` runs **once per worker**. Build state **outside** it, then
  `move` + `.clone()` inside, or each worker gets a private copy.
- Because `Data` is an `Arc` (shared `&T` only), mutable in-memory state needs **interior mutability** - 
  a `Mutex<...>` or `RwLock<...>` field. A real `sqlx::Pool` is already shareable, so no `Mutex` needed.
- ⚠️ Extracting a `web::Data<T>` you never registered panics at runtime with **"App data is not configured"** - 
  register the exact type the handler asks for.

## Quick check

```quiz
[
  {
    "q": "Why must web::Data be created outside the HttpServer::new closure?",
    "choices": [
      "The closure won't compile if Data is created inside it",
      "The closure runs once per worker thread, so state built inside gives each worker a separate copy",
      "web::Data can only be created in an async context",
      "It's a style preference with no functional effect"
    ],
    "answer": 1,
    "explain": "HttpServer::new calls its closure once per worker. Building state inside means every worker gets its own copy; build it once outside and clone the Arc in so all workers share one value."
  },
  {
    "q": "Your AppState holds an in-memory HashMap you need to write to. What does the field need?",
    "choices": [
      "Nothing - web::Data already allows mutation",
      "An interior-mutability wrapper like Mutex<...> or RwLock<...>",
      "The #[mut] attribute on the field",
      "A second web::Data registration"
    ],
    "answer": 1,
    "explain": "web::Data<T> is an Arc<T>, which only hands out shared &T references. To mutate through a shared reference you need interior mutability - a Mutex or RwLock field."
  },
  {
    "q": "A handler extracts web::Data<PgPool> but you only registered web::Data<AppState>. What happens?",
    "choices": [
      "A compile error pointing at the mismatch",
      "The handler receives a default-constructed PgPool",
      "A runtime panic: 'App data is not configured'",
      "The request silently returns 404"
    ],
    "answer": 2,
    "explain": "Extraction is by type. With no matching registered type, actix-web panics at runtime ('App data is not configured') on the first request - it's not caught at compile time."
  }
]
```


---

# Middleware

The mental model that carries the whole phase: **middleware in actix-web wraps your service.** Take your `App` (or a `web::scope`) and call `.wrap(...)`, which tucks your routes inside a new outer layer. A request travels inward through each wrapper before it reaches your handler, and the response travels back out through the same wrappers in reverse.

Picture an onion: each layer can look at the request on the way in, decide whether to keep going, and look at the response on the way out. That "on the way out" half is the payoff - one piece of middleware wraps the *entire* round trip. Logging, compression, auth, CORS all live in that wrapper.

> 📝 This is the same idea you'll meet in almost every web framework. If you've read the [axum guide](/guides/axum-from-zero), its `.layer()` is actix-web's `.wrap()` - the shape differs, the picture is identical. More on that contrast at the end.

We'll keep growing the **articles API** from the earlier phases. By the end it'll log every request and turn away anyone without an auth header.

## Where middleware fits

A request to your articles API doesn't hit `list_articles` directly - it passes through whatever you've wrapped around the `App`:

```mermaid
flowchart LR
  A[Request] --> B[Logger] --> C[Auth] --> D[Handler]
  D --> E[Response] --> C --> B --> F[Out the door]
```

The diagram is the entire concept. Everything that follows - the built-ins, the ordering rule that trips people up, your own custom middleware - is just *what* you put in those boxes and *which order* they sit in.

## The built-ins you'll reach for first

You rarely write middleware from scratch. actix-web ships the common ones in `actix_web::middleware`; the most useful is **`Logger`**, which logs every request: method, path, status, and time taken.

```rust
use actix_web::{middleware::Logger, web, App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .route("/articles", web::get().to(list_articles))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `.wrap(Logger::default())` wrapped the whole `App` in a logging layer. Every request to `/articles` now gets logged on arrival and on response, without touching `list_articles`. The catch: `Logger` *emits* log records, but something has to *print* them - that's `env_logger::init_from_env(...)`. Skip it and the logs go nowhere; the middleware isn't broken, nobody's listening.

> ⚠️ `Logger` produces nothing visible on its own. A logging facade (`env_logger`, `tracing-subscriber`, etc.) must be initialized once at startup. Missing logs almost always means a missing subscriber, not missing middleware.

Other built-ins drop in the same way:

```rust
use actix_web::middleware::{Compress, NormalizePath};

App::new()
    .wrap(Compress::default())              // gzip/brotli responses automatically
    .wrap(NormalizePath::trim())            // /articles/ and /articles both match
    .route("/articles", web::get().to(list_articles))
```

*What just happened:* `Compress` negotiates response compression from `Accept-Encoding` and compresses the body; `NormalizePath::trim()` strips trailing slashes so a stray `/articles/` still hits `/articles`. `DefaultHeaders` stamps headers like `X-Version` onto every response. Same move every time: construct it, hand it to `.wrap()`.

> 💡 **CORS lives in a separate crate.** Cross-origin headers aren't in `actix-web` core - add the **`actix-cors`** crate and wrap a `Cors`:
> ```rust
> use actix_cors::Cors;
>
> App::new()
>     .wrap(Cors::default().allow_any_origin())
>     .route("/articles", web::get().to(list_articles))
> ```
> `Cors::default()` is locked down on purpose (it allows almost nothing) - you opt into origins, methods, and headers explicitly with builder calls like `.allowed_origin("https://example.com")`. That strictness is a feature: you say exactly who's allowed in.

## ⚠️ The wrap-order rule everyone trips on

The single most confusing thing about actix-web middleware, so read it twice.

**Middleware runs as a stack. The LAST `.wrap()` you register is the OUTERMOST layer** - it runs *first* on the way in and *last* on the way out.

```rust
App::new()
    .wrap(Logger::default())   // registered first  → INNER
    .wrap(Compress::default()) // registered last   → OUTER, runs first
    .route("/articles", web::get().to(list_articles))
```

*What just happened:* even though `Logger` is written above `Compress`, `Compress` is the outermost wrapper because it was registered last. On the way in, the order is `Compress → Logger → handler`; on the way out it reverses. Read a stack of `.wrap()` calls **from the bottom up** to see the order a request actually travels.

Order changes behavior: to have `Logger` record the *final, compressed* response, `Compress` must be the outer layer (registered after `Logger`), exactly as above. Get it backwards and your logs describe a response that no longer matches what went over the wire. When middleware "isn't seeing" what you expect, suspect the order first.

## Writing your own with from_fn

When no built-in does what you need, the easy modern path (actix-web 4.4+) is **`middleware::from_fn`**, which turns a plain `async fn` into middleware. Your function receives the incoming `ServiceRequest` and a `Next` (the rest of the chain), and either calls `next.call(req).await` to continue or returns early to short-circuit.

Here's a timing middleware that logs how long each request took:

```rust
use actix_web::middleware::{from_fn, Next};
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::Error;

async fn timing(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let start = std::time::Instant::now();
    let res = next.call(req).await?;     // run the rest of the chain
    log::info!("{} {:?}", res.status(), start.elapsed());
    Ok(res)
}

// App::new().wrap(from_fn(timing))
```

*What just happened:* `from_fn(timing)` wraps `timing` into middleware you can `.wrap()`. `next.call(req).await?` hands control inward to the rest of the chain and gives back the `ServiceResponse`. Everything before that call runs on the way *in*; everything after runs on the way *out* - why we grab `start` before and log `elapsed()` after. One function straddles the whole round trip, like the onion picture.

Now an auth gate that rejects any request missing an `Authorization` header *before* it reaches a handler:

```rust
use actix_web::middleware::{from_fn, Next};
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::error::ErrorUnauthorized;
use actix_web::Error;

async fn require_auth(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    if req.headers().get("Authorization").is_none() {
        return Err(ErrorUnauthorized("missing Authorization header"));
    }
    next.call(req).await
}

// App::new().wrap(from_fn(require_auth))
```

*What just happened:* we inspect `req.headers()` first. If `Authorization` is absent, we return `Err(ErrorUnauthorized("..."))` - `next.call` is **never reached**, the handler never runs, and a `401 Unauthorized` goes straight back out. Otherwise we fall through to `next.call(req).await`. That early `return Err(...)` is the short-circuit - the whole point of middleware that can refuse a request.

> 💡 `from_fn` covers the vast majority of needs. For *stateful* middleware - something that holds its own data and must initialize per-worker - actix-web also exposes the lower-level **`Transform`** trait, implemented by hand with two structs plus `poll_ready` and `call`. Heavier and rarely necessary; reach for it only when `from_fn` genuinely can't carry the state you need.

## Same idea, different shape: actix-web vs axum/tower

If you've used [axum](/guides/axum-from-zero), none of this is new - only the spelling changed.

| | actix-web | axum / tower |
|---|---|---|
| Attach | `.wrap(thing)` | `.layer(thing)` |
| Custom | `middleware::from_fn` | `middleware::from_fn` |
| Continue | `next.call(req).await` | `next.run(req).await` |
| Ordering | last `.wrap()` = outermost | last `.layer()` = outermost |

*What just happened:* axum leans on tower's `Layer` abstraction, so its middleware also works with HTTP clients and gRPC; actix-web's is its own thing, tuned to actix-web. Different ecosystems, identical mental model - wrap the service, run the chain as a stack, short-circuit when you must. Learn it once, it transfers.

## Recap

- **Middleware wraps your service** - attach it with **`.wrap(...)`** on an `App` or a `web::scope`; the chain runs as a stack of onion layers around your handler.
- **Built-ins** live in `actix_web::middleware`: **`Logger`** (pair it with `env_logger` or no logs print), **`Compress`**, **`NormalizePath`**, **`DefaultHeaders`**. **CORS** comes from the separate **`actix-cors`** crate via `Cors::default()...`.
- ⚠️ **The order rule:** the **last** `.wrap()` registered is the **outermost** - runs first on the way in, last on the way out. Read a `.wrap()` stack bottom-up.
- **`middleware::from_fn`** turns an `async fn(ServiceRequest, Next)` into custom middleware: call `next.call(req).await` to continue, or return `Err(ErrorUnauthorized(...))` to short-circuit before the handler runs.
- The heavier **`Transform`** trait exists for stateful middleware, but `from_fn` handles most cases - the pattern mirrors axum/tower's `.layer()`.

## Quick check

```quiz
[
  {
    "q": "How do you attach middleware to an actix-web App?",
    "choices": [".layer(...) on the App", ".wrap(...) on the App or a web::scope", ".use(...) on the HttpServer", "A middleware: field in the App config"],
    "answer": 1,
    "explain": "actix-web middleware wraps the service: you attach it with .wrap(...) on an App or a web::scope. (.layer() is axum/tower's spelling of the same idea.)"
  },
  {
    "q": "You write `.wrap(Logger::default()).wrap(Compress::default())`. Which one is the OUTERMOST layer (runs first on the way in)?",
    "choices": ["Logger, because it's written first", "Compress, because the last .wrap() is the outermost", "Neither - order is undefined", "Both run at the same time"],
    "answer": 1,
    "explain": "Middleware runs as a stack: the LAST .wrap() registered (Compress here) is the outermost, so it runs first on the way in and last on the way out. Read a .wrap() stack from the bottom up."
  },
  {
    "q": "In a from_fn middleware, how do you reject a request so the handler never runs?",
    "choices": ["Call next.call(req).await as usual", "Return early with an Err, e.g. Err(ErrorUnauthorized(\"...\")), instead of calling next.call", "Panic inside the function", "Return Ok with an empty ServiceResponse"],
    "answer": 1,
    "explain": "Returning early - for example Err(ErrorUnauthorized(\"...\")) - short-circuits the chain before next.call is ever reached, so the handler never runs and the error response goes straight back out."
  }
]
```


---

# A REST API with Error Handling

Everything else was building toward this. You have an `App`, routing, extractors, responders, shared
state, and middleware. Now wire them into a real REST resource - full CRUD over the `articles` store - 
and fix a pain you've been quietly living with since Phase 3.

## The mental model

Two ideas, and the rest of this phase writes itself.

**One: a REST resource is just five handlers over the shared store.** "Articles" isn't some special
construct - it's a list endpoint, a show endpoint, a create, an update, and a delete, all reaching through
the same `web::Data<AppState>` you built in [Phase 4](04-shared-state.md). The HTTP method plus the path
decides which handler runs; the body of each handler is a small read or write against that `Mutex<HashMap>`.

**Two: the clean way to vary status is to return a `Result` and let actix render the error.** Back in
Phase 3 you likely hit this wall: one branch returns `HttpResponse::Ok().json(...)`, another wants
`HttpResponse::NotFound().finish()`, and the types don't line up unless you match and rebuild responses by
hand in every handler. actix's answer is to push the error *out of* the happy path. Your handler returns
`Result<HttpResponse, ApiError>`, the success arm builds the 200, and **a returned `Err` becomes the HTTP
response automatically** - because your error type implements the `ResponseError` trait. One place defines
what a "not found" looks like on the wire; every handler just says `?`.

> 💡 If you've read the [axum guide](/guides/axum-from-zero), this is the same shape under a different name.
> axum has you implement `IntoResponse` on your error type; actix has you implement `ResponseError`. Both
> turn "a value my handler returned" into "an HTTP response." Once you see one, you've seen both.

## The five handlers

Five functions, mounted on a `web::scope("/api/v1")` so the version prefix lives in one place. Each reads
or writes the shared store and returns `Result<HttpResponse, ApiError>` (we'll define `ApiError` next - 
read these first to see *why* we want it):

```rust
use actix_web::{web, App, HttpServer, HttpResponse};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;

#[derive(Clone, Serialize, Deserialize)]
struct Article {
    id: u32,
    title: String,
    body: String,
}

// what the client sends on create/update - no id, the server owns that
#[derive(Deserialize)]
struct ArticleInput {
    title: String,
    body: String,
}

struct AppState {
    articles: Mutex<HashMap<u32, Article>>,
    next_id: Mutex<u32>,
}

// GET /api/v1/articles  → 200 with the list
async fn list(state: web::Data<AppState>) -> Result<HttpResponse, ApiError> {
    let map = state.articles.lock().unwrap();
    let articles: Vec<Article> = map.values().cloned().collect();
    Ok(HttpResponse::Ok().json(articles))
}

// GET /api/v1/articles/{id}  → 200, or 404 if missing
async fn show(
    path: web::Path<u32>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    let map = state.articles.lock().unwrap();
    let article = map.get(&path).cloned().ok_or(ApiError::NotFound)?;
    Ok(HttpResponse::Ok().json(article))
}

// POST /api/v1/articles  → 201 with the created article
async fn create(
    body: web::Json<ArticleInput>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    let input = body.into_inner();
    if input.title.trim().is_empty() {
        return Err(ApiError::BadRequest("title must not be empty".into()));
    }

    let mut id_guard = state.next_id.lock().unwrap();
    let id = *id_guard;
    *id_guard += 1;

    let article = Article { id, title: input.title, body: input.body };
    state.articles.lock().unwrap().insert(id, article.clone());
    Ok(HttpResponse::Created().json(article))
}

// PUT /api/v1/articles/{id}  → 200, or 404 if missing
async fn update(
    path: web::Path<u32>,
    body: web::Json<ArticleInput>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    let id = path.into_inner();
    let input = body.into_inner();
    let mut map = state.articles.lock().unwrap();

    let article = map.get_mut(&id).ok_or(ApiError::NotFound)?;
    article.title = input.title;
    article.body = input.body;
    Ok(HttpResponse::Ok().json(article.clone()))
}

// DELETE /api/v1/articles/{id}  → 204, or 404 if missing
async fn delete(
    path: web::Path<u32>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    let mut map = state.articles.lock().unwrap();
    map.remove(&path.into_inner()).ok_or(ApiError::NotFound)?;
    Ok(HttpResponse::NoContent().finish())
}
```

*What just happened:* five handlers, one shared store. `show`, `update`, and `delete` share the same
"look it up, `?` away the `NotFound`" move - `map.get(...).ok_or(ApiError::NotFound)?` reads as "give me the
article or bail out with a 404," and the bail-out *is* the response. `create` mints an id from the `next_id`
counter (a second `Mutex`, locked independently of the map) and returns `201 Created`. `delete` returns
`204 No Content` via `.finish()` - no body. Notice what's *not* here: no `match` rebuilding error responses,
no juggling `HttpResponse` types - the error arm left the building via `?`.

Now mount them. The five handlers live on a scope, and the state is built once and cloned in per worker,
exactly the pattern from Phase 4:

```rust
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = web::Data::new(AppState {
        articles: Mutex::new(HashMap::new()),
        next_id: Mutex::new(1),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .service(
                web::scope("/api/v1")
                    .route("/articles", web::get().to(list))
                    .route("/articles", web::post().to(create))
                    .route("/articles/{id}", web::get().to(show))
                    .route("/articles/{id}", web::put().to(update))
                    .route("/articles/{id}", web::delete().to(delete)),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

*What just happened:* `web::scope("/api/v1")` prefixes every route inside it, so `/articles` becomes
`/api/v1/articles`. The same path string carries multiple methods - `web::get()` and `web::post()` on
`/articles` are two distinct routes. `state.clone()` hands each worker a handle to the *one* `AppState`,
the load-bearing detail from Phase 4.

## The error type: one shape, defined once

Everything above leaned on `ApiError`. Here's the whole thing - an enum of the failures this API can
produce, plus the two trait impls teaching actix how to turn it into an HTTP response:

```rust
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use std::fmt;

#[derive(Debug)]
enum ApiError {
    NotFound,
    BadRequest(String),
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl ResponseError for ApiError {
    fn status_code(&self) -> StatusCode {
        match self {
            ApiError::NotFound => StatusCode::NOT_FOUND,
            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .json(serde_json::json!({ "error": self.to_string() }))
    }
}
```

*What just happened:* `ResponseError` is the whole trick. When a handler returns `Err(ApiError::NotFound)`,
actix calls `error_response()` on it and sends back whatever that produces - the right status with a
consistent `{"error": "..."}` body. `status_code()` maps each variant to its HTTP status; the default
`error_response()` would use plain text, but we override it so every error speaks the same JSON dialect.
`Display` is required by the trait, cheaply derived off `Debug` here. Define this once and every handler
returning `Result<_, ApiError>` gets it for free.

> ⚠️ `ResponseError` requires your type to be `Display + Debug`. If the compiler complains that your error
> "doesn't implement `ResponseError`," check that you actually impl'd `Display` - that's the usual missing
> piece, and the error message can point at the wrong line.

## `?`, `From`, and foreign errors

The `?` operator does quiet, important work. On a `Result<T, ApiError>` it's a clean early return - but it
has a superpower: it converts error types *on the way out* if there's a `From` impl connecting them. That's
how `?` swallows errors from libraries that know nothing about your `ApiError`.

Say a handler parses something and gets a `std::num::ParseIntError`. Teach `ApiError` how to absorb it:

```rust
impl From<std::num::ParseIntError> for ApiError {
    fn from(_: std::num::ParseIntError) -> Self {
        ApiError::BadRequest("invalid number".into())
    }
}

async fn show_by_str(
    path: web::Path<String>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    let id: u32 = path.parse()?; // ParseIntError → ApiError via From, then ? returns it
    let map = state.articles.lock().unwrap();
    let article = map.get(&id).cloned().ok_or(ApiError::NotFound)?;
    Ok(HttpResponse::Ok().json(article))
}
```

*What just happened:* `path.parse()` returns `Result<u32, ParseIntError>`. Because `From<ParseIntError>` for
`ApiError` exists, `?` converts the foreign error into an `ApiError::BadRequest` - which `ResponseError`
renders as a 400. One `From` impl and every `?` on a `ParseIntError` maps to a sensible response. This is
how real handlers stay short: DB errors, parse errors, and your own all funnel through `?` into one type.

> 💡 Writing `Display` and `From` impls by hand gets tedious as the enum grows. The **`thiserror`** crate
> generates both from attributes - annotate each variant with a `#[error("...")]` message and add `#[from]`
> to a field to auto-derive the conversion. Same mental model; `thiserror` just deletes the boilerplate. It's
> the standard choice once an API has more than a couple of error variants.

## ⚠️ Extractor failures are already errors - don't panic

One thing the framework handles before your code runs: if a client POSTs malformed JSON, the
`web::Json<ArticleInput>` extractor fails *during extraction* and actix returns a `400 Bad Request` on its
own - you never see a bad body inside your handler. The principle: **a handler that can't proceed should
return an error, never panic.** A `.unwrap()` on something a client controls is a 500 waiting to happen.

If you want the default extractor error to match your JSON error shape, configure it with `app_data`:

```rust
use actix_web::web::JsonConfig;

// inside the App builder, alongside .app_data(state.clone()):
.app_data(JsonConfig::default().error_handler(|err, _req| {
    let msg = err.to_string();
    actix_web::error::InternalError::from_response(
        err,
        HttpResponse::BadRequest().json(serde_json::json!({ "error": msg })),
    )
    .into()
}))
```

*What just happened:* `JsonConfig`'s `error_handler` intercepts extractor-level JSON failures and returns a
custom response - the same `{"error": "..."}` shape your `ResponseError` produces. Without this you still
get a 400 on bad JSON; this just matches the body to your house style.

## Take it for a spin

With the server running, exercise the full lifecycle with `curl`:

```bash
# create one → 201 with the new article (including its server-assigned id)
curl -s -X POST http://127.0.0.1:8080/api/v1/articles \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","body":"first post"}'

# list them → 200 with a JSON array
curl -s http://127.0.0.1:8080/api/v1/articles

# fetch one that doesn't exist → 404 {"error":"NotFound"}
curl -s http://127.0.0.1:8080/api/v1/articles/999

# update it → 200 with the new values
curl -s -X PUT http://127.0.0.1:8080/api/v1/articles/1 \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello (edited)","body":"updated"}'

# delete it → 204, empty body
curl -s -i -X DELETE http://127.0.0.1:8080/api/v1/articles/1
```

*What just happened:* you drove all five handlers and saw all four status codes (200, 201, 204, 404) without
a hand-built error branch in your handler bodies - `Result` + `ResponseError` rendered the 404s for you. A
bad JSON body gets you the 400 the extractor (or your `JsonConfig`) produces.

> 💡 The `Mutex<HashMap>` is a teaching prop, not a database. To go real, swap the handler bodies for `sqlx`
> queries against the `PgPool` from Phase 4 and add a `From<sqlx::Error>` impl so DB failures `?` straight
> into `ApiError` as 500s. The error *model* doesn't change - only what happens between the lock and the
> response does.

## Recap

- A **REST resource is five handlers over the shared store** - list, show, create, update, delete - 
  mounted on `web::scope("/api/v1")`, each reaching through `web::Data<AppState>` from Phase 4.
- Handlers return **`Result<HttpResponse, ApiError>`** and use **`?`**; a returned `Err` becomes the HTTP
  response, so you stop juggling response types across branches.
- The **`ResponseError` trait** (with `Display`) defines status and body in **one place** - 
  `status_code()` maps variants to codes, `error_response()` gives every error the same JSON shape.
- **`From<E>` impls** let `?` convert foreign errors (parse, DB) into your `ApiError`; **`thiserror`**
  generates the `Display`/`From` boilerplate once the enum grows.
- ⚠️ Extractor failures (bad JSON) already produce **400s automatically** - customize the response with
  `JsonConfig::error_handler` if you want it to match your shape.

## Quick check

```quiz
[
  {
    "q": "A handler returns Result<HttpResponse, ApiError> and returns Err(ApiError::NotFound). How does that become a 404 response?",
    "choices": [
      "actix checks the variant name and guesses the status",
      "ApiError implements ResponseError, so actix calls its status_code/error_response to render the Err",
      "The ? operator sets the status code directly",
      "You must add a match in the handler to convert it"
    ],
    "answer": 1,
    "explain": "Implementing ResponseError on your error type teaches actix how to turn a returned Err into an HTTP response - status_code() and error_response() define the status and body once, for every handler."
  },
  {
    "q": "Why does writing impl From<std::num::ParseIntError> for ApiError help in a handler?",
    "choices": [
      "It makes ParseIntError print nicer in logs",
      "It lets the ? operator auto-convert a ParseIntError into your ApiError on the way out",
      "It is required before you can call .parse()",
      "It changes the HTTP status of every response to 400"
    ],
    "answer": 1,
    "explain": "The ? operator converts error types when a From impl connects them. With From<ParseIntError> for ApiError, a ? on a parse result returns your ApiError, which ResponseError then renders."
  },
  {
    "q": "A client POSTs malformed JSON to your create handler. What happens by default?",
    "choices": [
      "Your handler runs with an empty struct",
      "The web::Json extractor fails during extraction and actix returns a 400 before your handler runs",
      "The request panics and the server crashes",
      "actix returns a 500 because there is no body"
    ],
    "answer": 1,
    "explain": "Extractor failures are handled before your handler body - bad JSON makes web::Json fail and actix returns a 400 on its own. You can customize that response with JsonConfig::error_handler, but you should never panic on client input."
  }
]
```


---

# Testing & Production

You've grown the articles API the whole way - `App`, `HttpServer`, extractors, responders, shared state, middleware, and a full CRUD layer with `ResponseError`. Now comes the part that decides whether anyone trusts it: proving it works, and running it somewhere real without it falling over at 3am.

## The mental model: testing is calling your app in memory - no ports

The fact that makes actix-web pleasant to test: you never start a real server. The `actix_web::test` module builds your `App` into an in-memory service and pushes requests straight through it. No socket opens, no port binds, no background task to remember to shut down. You hand the app a request, it produces a response, you read it back - all inside the test process, in microseconds.

> 💡 A test is just: build the same `App` your real server runs, turn it into a service with `test::init_service`, craft a request with `test::TestRequest`, and call `test::call_service`. The entire chain - middleware, routing, extractors, your handler - runs exactly as it would for a live request, except nothing leaves the process.

```rust
use actix_web::{test, App, web};

#[actix_web::test]
async fn list_articles_ok() {
    let app = test::init_service(
        App::new().app_data(state()).route("/articles", web::get().to(list))
    ).await;
    let req = test::TestRequest::get().uri("/articles").to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success());
}
```

*What just happened:* `#[actix_web::test]` does for `main` what `#[actix_web::main]` does - spins up the actix runtime so the `async` test can `.await`. `test::init_service` takes the *same* `App` builder your server uses and compiles it into an in-memory service. `test::TestRequest::get().uri("/articles").to_request()` builds a real `Request` with no connection behind it. `test::call_service(&app, req)` runs the whole pipeline and hands back the `ServiceResponse`, whose `.status()` we assert - under a millisecond, never touching the network.

Testing a **POST with a JSON body** is the same shape with two helpers - `set_json` to attach the body (it sets `Content-Type: application/json` for you) and `read_body_json` to deserialize the response so you can assert on its contents:

```rust
use actix_web::{test, App, web};

#[actix_web::test]
async fn create_article_returns_it() {
    let app = test::init_service(
        App::new().app_data(state()).route("/articles", web::post().to(create))
    ).await;

    let req = test::TestRequest::post()
        .uri("/articles")
        .set_json(&serde_json::json!({ "title": "write tests" }))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), 201);

    let body: Article = test::read_body_json(resp).await;
    assert_eq!(body.title, "write tests");
}
```

*What just happened:* `set_json(&body)` serializes the value and sets the JSON content type, so `web::Json<T>` sees a properly-formed request. We assert `201`, then `test::read_body_json(resp).await` deserializes the response into an `Article` to check its field. Tighter helpers exist too: `test::call_and_read_body_json` does call-and-deserialize in one step, and `test::try_call_service` returns a `Result` instead of panicking.

> 📝 For inputs you *expect* to fail - a missing title, malformed JSON - send the bad payload and assert the status and error body your `ResponseError` impl produces. That's where Phase 6's error handling pays off: tests confirm clients get a `400`, not a stack trace.

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

## Share routes between `main` and tests with `.configure()`

The two tests above share a smell: each re-declares its routes. The moment your real `App` and your test `App` describe routes *differently*, your tests validate wiring production doesn't use. The fix: declare routes once, in a function, and call it from both places.

actix-web's hook for this is **`.configure(config_fn)`**, where `config_fn` takes a `&mut web::ServiceConfig` and registers everything on it.

```rust
use actix_web::{web, App, HttpServer};

fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/articles")
            .route("", web::get().to(list))
            .route("", web::post().to(create))
            .route("/{id}", web::get().to(get_one))
            .route("/{id}", web::delete().to(delete)),
    );
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().app_data(state()).configure(config))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}
```

And the test calls the very same `config`:

```rust
#[actix_web::test]
async fn list_articles_ok() {
    let app = test::init_service(
        App::new().app_data(state()).configure(config)
    ).await;
    let req = test::TestRequest::get().uri("/articles").to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success());
}
```

*What just happened:* all route registration now lives in one `config` function. `main` and the test both call the *same* `.configure(config)` - no second, slightly-different set of routes that "should match production" but quietly drifts. If handlers need state or a pool, pass it in (`config(pool)` returning a closure, or set `app_data` alongside `configure`) so tests can hand in a fixture. Rule of thumb: the first time you copy-paste route setup into a test, pull out a `.configure()` function.

## Production: workers, graceful shutdown, env config

actix-web is built for production out of the box - a lot of what you'd hand-roll in other stacks is already there. Three things to know.

**Workers.** `HttpServer` runs multiple copies of your `App`, one per worker thread, and load-balances connections across them. By default it spawns **one worker per logical CPU**, which is usually what you want. You only set `.workers(n)` to override it - for example, pinning it lower in a small container.

```rust
HttpServer::new(|| App::new().app_data(state()).configure(config))
    .workers(4)
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
```

*What just happened:* `.workers(4)` tells `HttpServer` to run four worker threads, each with its own copy of the `App` built by your closure - that's why the `App` is built in a closure, constructed once per worker. Remove `.workers(4)` and you get the default: one per CPU. Note `0.0.0.0` rather than `127.0.0.1` - in a container you bind all interfaces so outside traffic reaches you.

**Graceful shutdown - already handled.** actix-web installs signal handlers for you. On `SIGINT` (Ctrl+C) or `SIGTERM` (what your platform sends on a deploy or scale-down), it **stops accepting new connections, lets in-flight requests finish, then exits** - no goroutine-and-channel dance, it's built into `.run()`. The one knob you may tune is how long it waits for stragglers:

```rust
HttpServer::new(|| App::new().configure(config))
    .shutdown_timeout(30) // seconds to let in-flight requests drain
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
```

*What just happened:* `.shutdown_timeout(30)` gives in-flight requests up to 30 seconds after a shutdown signal; past that, remaining connections are force-closed so a stuck request can't block your deploy forever. The default is already 30 seconds - set this only to lengthen (long uploads) or shorten (fast restarts) it.

**Env config and logging.** Read anything that changes between environments - `PORT`, `DATABASE_URL`, secrets - from the environment, not hard-coded constants. Recall from Phase 5 that `Logger` writes through the `log` facade, which does nothing until a logger is initialized - call `env_logger::init()` at the top of `main`, or your access logs stay silent in production.

```rust
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init(); // now the Logger middleware actually prints

    let port: u16 = std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(8080);

    HttpServer::new(|| App::new().app_data(state()).wrap(actix_web::middleware::Logger::default()).configure(config))
        .bind(("0.0.0.0", port))?
        .run()
        .await
}
```

*What just happened:* `env_logger::init()` reads `RUST_LOG` (e.g. `RUST_LOG=info`) and wires up the logger `Logger` middleware needs - skip it and your access logs are silent. We default `PORT` to `8080` but let the environment override it, so a platform injecting `PORT=10000` works with no code change.

## Deploy shape: release build, a small container, a proxy in front

Three steps take this from "runs on my machine" to "runs in production."

First, build in **release mode** - debug builds are unoptimized and slow; production wants the optimized binary:

```bash
cargo build --release
# produces target/release/articles-api
```

*What just happened:* `--release` turns on optimizations (and strips debug assertions), producing a much faster binary. Compilation takes longer, but only once at build time, not per request.

Second, package it in a **multi-stage container** - compile in a stage with the full Rust toolchain, then copy *only* the binary into a tiny runtime image:

```bash
# Build stage - full Rust toolchain
FROM rust:1 AS build
WORKDIR /src
COPY . .
RUN cargo build --release

# Run stage - slim image, just the binary
FROM debian:stable-slim
COPY --from=build /src/target/release/articles-api /usr/local/bin/articles-api
ENV RUST_LOG=info
EXPOSE 8080
CMD ["articles-api"]
```

*What just happened:* the first stage has the whole Rust toolchain and compiles the binary; the second is a slim Debian image carrying just the executable (plus the few shared libraries a default Rust binary links against - why `debian:stable-slim` rather than `scratch`). Small image, tiny attack surface. `RUST_LOG=info` bakes logging on by default.

Third, put a **reverse proxy** in front - nginx, Caddy, or whatever your platform provides. The proxy terminates TLS, can serve static assets, and load-balances across instances. Your actix-web app speaks plain HTTP on its port; the proxy faces the public internet - don't terminate TLS in actix-web itself.

That's the whole deploy shape: a release binary, a small container, env-var config, behind a proxy. The rest - picking a host, wiring CI, domain and TLS specifics - is covered in [ship your side project](/guides/ship-your-side-project).

## Recap

- A test is **calling your `App` in memory**: `test::init_service(App::new()...)`, build a request with `test::TestRequest`, run it with `test::call_service`, assert on `.status()`. No ports, no network. For JSON POSTs use `.set_json(&body)` and read the response with `test::read_body_json` (or the one-shot `test::call_and_read_body_json`).
- Declare routes **once** in a `fn config(cfg: &mut web::ServiceConfig)` and call `.configure(config)` from both `main` and your tests - one source of truth, no drift.
- `HttpServer` runs **one worker per CPU by default**; override with `.workers(n)` only when you need to.
- **Graceful shutdown is built in** - actix-web handles `SIGINT`/`SIGTERM`, draining in-flight requests before exit. Tune the drain window with `.shutdown_timeout(secs)`; you rarely need to do more.
- Read config (`PORT`, `DATABASE_URL`) from the **environment**, and call `env_logger::init()` so the `Logger` middleware actually prints.
- Ship a **release build** (`cargo build --release`) in a **multi-stage container**, behind a **reverse proxy** that terminates TLS.

## Quick check

Lock in the core fact (testing is in-memory) and the two production must-haves:

```quiz
[
  {
    "q": "How does actix_web::test run a request against your app?",
    "choices": ["It starts a real server on a random port and sends an HTTP request over the loopback interface", "It builds the App into an in-memory service with test::init_service and runs the request through it with test::call_service - no port, no socket", "It mocks the TCP stack at the OS level", "It can only test individual handler functions in isolation, never the full app"],
    "answer": 1,
    "explain": "test::init_service compiles the same App into an in-memory service; test::call_service pushes a TestRequest through the full middleware-routing-handler chain in-process. Nothing binds a port or opens a socket."
  },
  {
    "q": "What does .configure(config_fn) let you do?",
    "choices": ["Enable release-mode optimizations", "Set the number of worker threads", "Register routes once in a fn taking &mut web::ServiceConfig, then call it from both main and tests so they share identical wiring", "Configure TLS certificates"],
    "answer": 2,
    "explain": "A config function registers services/routes on the ServiceConfig. Calling .configure(config) in both main and tests means there's one source of truth for routing - production and tests can't drift apart."
  },
  {
    "q": "What do you have to write yourself to get graceful shutdown in actix-web?",
    "choices": ["A goroutine plus a signal channel that calls server.shutdown() on SIGTERM", "Nothing - it's built in; actix-web handles SIGINT/SIGTERM and drains in-flight requests, and you only optionally tune .shutdown_timeout(secs)", "A custom middleware that intercepts the shutdown signal", "A reverse proxy that drains connections before killing the process"],
    "answer": 1,
    "explain": "HttpServer's .run() installs signal handlers and stops accepting new connections, lets in-flight requests finish, then exits - automatically. The only knob is .shutdown_timeout to lengthen or shorten the drain window."
  }
]
```


---

# Where to Go Next

Look back at the ground you covered. You can stand up an `App` and run it across worker threads with `HttpServer`, route a request to a handler, pull pieces out with extractors like `Path`, `Query`, `Json`, and `web::Data`, return anything that implements `Responder`, share a store across every worker without a global, wrap it in middleware, build full CRUD, and turn errors into clean responses with `ResponseError` - then test it with `actix_web::test` and tune it for production. That's a real REST API, not a toy.

The quieter win: the shape underneath actix-web is the same shape in every serious Rust web framework - an **`App`** of routes, run by an **`HttpServer`**, with handlers that **extract from the request and return a `Responder`**. Learn it once and you can read the others on sight. This last phase is the map: where actix-web sits among its neighbors, the layer you'll almost certainly add next, the root worth knowing, and one thing to go build.

## actix-web vs the field

You now know enough to choose a framework *on purpose* rather than by reputation. The good news in Rust: the big three are all production-grade and all fast. The differences are about *feel* and *what you build on*, not whole different universes.

```mermaid
flowchart TD
  Start[Need a Rust web service?] --> Q{What do you value most?}
  Q -- Max performance + maturity + batteries --> Actix[actix-web]
  Q -- Tower ecosystem + type-safe extractors --> Axum[axum]
  Q -- Most ergonomic, macro-driven --> Rocket[Rocket]
  Actix --> Note[You are here]
```

A line on each:

- **actix-web** - the mature, batteries-included heavyweight, consistently at or near the **top of the performance benchmarks**: a long track record, a deep feature set (websockets, sessions, all in the box), and the `web::Data`/`ResponseError` patterns you just learned. (You are here.)
- **axum** - the newer option from the Tokio team, tower-native and driven by the **type system**: plain `async fn` handlers, extractors as arguments, `IntoResponse` returns. Its superpower is the **tower** middleware ecosystem - reusable, and shared with gRPC via tonic. See [axum From Zero](/guides/axum-from-zero).
- **Rocket** - the most **ergonomic**, leaning hard on macros (`#[get("/")]` and friends) for concise handler code. See [Rocket From Zero](/guides/rocket-from-zero).

> 💡 How to pick: **actix-web** for maximum performance, maturity, and a big feature set out of the box. **axum** for the tower ecosystem and type-safe extractors. **Rocket** for concise, macro-driven code.

📝 Notice how much these three have *converged* - whichever you opened, you'd be writing the same thing: extract from the request, return a responder. Picking one is far less of a fork-in-the-road than it looks; none is "the best," the question is "best for *this* job."

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

Every API in this guide kept its books in memory - perfect for learning, useless in production, since a restart wipes the data. The next thing almost every real actix-web service grows is a **database**.

Rust has **no single default ORM**. Three common answers, each with a clear personality:

- **`sqlx`** - not an ORM, but the most popular companion. Write **raw SQL**; a macro checks your queries **against a real database at compile time**, so a typo'd column is a build error, not a 500 at 3am. Fully async.
- **SeaORM** - a proper **async ORM** built on sqlx, for entities, relations, and a query builder instead of hand-written SQL.
- **Diesel** - the **mature, established** ORM with a rich type-safe query DSL. More sync-flavored roots, worth knowing if the rest of your stack is async.

The reassuring bit: your handlers barely change. Remember Phase 4's `web::Data<T>`? A **`sqlx::PgPool`** drops straight into that same slot. Handlers still extract the pool, run a query, and return a `Responder` - you're swapping the bottom layer, not rewriting the top.

> 💡 Don't forget what's in the box: **websockets** are a long-standing actix-web strength. The day you want live updates - a chat feed, a pushing dashboard - it's right there in the framework you already know.

## The root: Tokio

actix-web doesn't float in the air. Underneath it, like every async Rust web framework, sits **Tokio** - the async runtime driving your `async fn`s, scheduling work across worker threads, handling I/O. Every `.await` ultimately answers to it.

You don't need to study Tokio to ship. But the day you want to know *why* an extractor can pause and resume, or how workers really share a pool, that's the floor dropping away. See [Tokio: The Async Runtime](/guides/tokio-the-async-runtime).

## What to build

Reading more won't make this stick - building one real thing will. Take the **articles API** you grew across this guide and carry it home:

- **Swap the in-memory store for sqlx + Postgres** so articles survive a restart. Drop a `PgPool` into `web::Data`, write a few compile-checked queries.
- **Add JWT (or session) auth middleware** so each request proves who it is, and articles belong to a user - the Phase 5 pattern, aimed at a real job.
- **Wire up `tracing`** for observability and a few metrics.
- **Tidy up config** so secrets, the database URL, and the port come from the environment.
- **Deploy it** somewhere you can hit from your phone.

If the articles API feels too familiar, build something new instead - a **URL shortener**, a **notes API**, or a tiny **live chat** that puts actix's websockets to use. Same muscles either way. Finishing one project completely teaches more than three more tutorials would.

## The clear-eyed close

actix-web was never magic. Strip it back and it's ideas you now understand completely: an **`App`** of routes, run by an **`HttpServer`**, with handlers that **extract from the request and return a `Responder`** - wrapped in middleware, error-handled with `ResponseError`, sitting on Tokio.

That's mature, fast Rust, and you can read the machine now. Go finish the articles API, give it a real database, lock it behind auth, light it up with tracing, deploy it, and show someone. You're ready.

## Recap

1. **You can ship a real actix-web API** - routed, extracted, responded, state-shared with `web::Data`, middleware-wrapped, error-handled with `ResponseError`, tested, and tuned for production.
2. **Choose a framework on purpose** - actix-web for maximum performance, maturity, and batteries (websockets included), axum for the tower ecosystem and type-safe extractors, Rocket for concise macro-driven code. They've largely converged, so the mental model transfers.
3. **A database is the next layer, and Rust has no single default** - sqlx (compile-checked raw SQL), SeaORM (async ORM), or Diesel (mature ORM). A `sqlx::PgPool` drops into the `web::Data` slot from Phase 4, so your handlers barely change.
4. **Lean on what's in the box** - actix-web's websockets are a real strength when you need live updates.
5. **Tokio is the root** - the async runtime driving every `.await` and every worker; learn it to remove the last of the magic.
6. **Build and finish one thing** - carry the articles API to sqlx + Postgres, JWT auth, tracing, real config, and a deploy.

## Quick check

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

```quiz
[
  {
    "q": "You want maximum performance, a long production track record, and websockets already built in. Which framework fits on purpose?",
    "choices": [
      "Rocket, because it uses the most macros",
      "actix-web, the mature, batteries-included benchmark leader",
      "axum, because it's the newest",
      "None of them support websockets"
    ],
    "answer": 1,
    "explain": "actix-web is the mature, batteries-included heavyweight at or near the top of the benchmarks, with websockets in the box. Pick axum for the tower ecosystem, Rocket for concise macro-driven code."
  },
  {
    "q": "What is the real situation with ORMs in Rust for an actix-web API?",
    "choices": [
      "actix-web ships its own official ORM you must use",
      "There's no single default - sqlx (compile-checked raw SQL), SeaORM (async ORM), and Diesel (mature ORM) are the common choices",
      "Only Diesel works with async Rust",
      "Rust web apps can't use a database"
    ],
    "answer": 1,
    "explain": "Rust has no single default ORM. sqlx checks raw SQL at compile time, SeaORM is an async ORM on top of it, and Diesel is the mature, more sync-flavored ORM. You choose based on the job."
  },
  {
    "q": "You're swapping the in-memory store for sqlx + Postgres. Why do your handlers barely change?",
    "choices": [
      "Because actix-web rewrites handlers automatically when you add a database",
      "Because a sqlx::PgPool drops into the same web::Data slot from Phase 4, so handlers still extract it, query, and return a Responder",
      "Because you have to abandon web::Data and use globals instead",
      "They don't - every handler must be rewritten from scratch"
    ],
    "answer": 1,
    "explain": "Phase 4's web::Data pattern pays off: a PgPool goes into web::Data just like the in-memory store did. Handlers still extract the pool with web::Data<T>, run a query, and return a Responder - the bottom layer changes, the top stays."
  }
]
```
