Just added: Algorithms you can run and practice
Updated Jul 10, 2026 Edit on GitHub

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 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:

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.

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:

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():

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.

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 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:

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:

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. 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."

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's Result / ResponseError).
  • Choose impl Responder for single-shape, terse handlers; HttpResponse for control and branchy handlers.

Quick check

[
  {
    "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."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. 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?

2. What status does returning web::Json(value) produce?

3. You want a 204 No Content response after a successful delete. Which finisher fits?