Responders
Here's the mental model, and it's the whole chapter in one sentence: a handler returns a value, and actix-web only accepts that value if its type knows how to become an HTTP response. That "knows how to become a response" is a trait called Responder. You don't call it yourself — you return a type that implements it, and the framework does the conversion.
So the question for every handler stops being "how do I write a response?" and becomes "what type do I return, and does it implement Responder?" Once you see it that way, the rest is picking the right type 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.
📝 In Phase 2 you learned the input half of a handler — extractors pull
Path,Query, andJsonout of the request. This chapter is the output half. Input by extractor, output byResponder: 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 Serialize;
What just happened: #[derive(Serialize)] is from serde, and it's the one prerequisite for sending a struct as JSON — it teaches Article how to turn itself into a JSON object. Without it, none of the .json() calls below would compile. Everything in this chapter assumes that derive is present.
HttpResponse: the workhorse
HttpResponse is a builder. You start with a status, optionally attach a body, and you're done. The status comes from a named method (Ok(), Created(), NotFound(), and friends), and then you finish the response one of three ways: .json(&value) to serialize a body as JSON, .body("…") to send a raw body, or .finish() to send no body at all.
use ;
async
What just happened: HttpResponse::Ok() starts a 200 OK response, and .json(&article) serializes the struct into the body and sets the Content-Type: application/json header for you. The return type is impl Responder because HttpResponse implements Responder — we'll unpack that phrase at the end of the chapter. For now, read it as "this function returns something that can become a response."
Returning a list is the same move — serde serializes a Vec<Article> into a JSON array:
use ;
async
What just happened: nothing new about HttpResponse here — the point is that .json() serializes anything that's Serialize, including a Vec. A list of articles becomes a JSON array with no extra ceremony.
The reason HttpResponse is the workhorse is the rest of the status methods. Each one is a different status code, and they pair naturally with .json(), .body(), or .finish():
use HttpResponse;
// 201 Created — return the thing you just made.
Created.json;
// 204 No Content — success, nothing to send back (e.g. a DELETE).
NoContent.finish;
// 404 Not Found — no body needed.
NotFound.finish;
// 400 Bad Request — a plain-text explanation.
BadRequest.body;
What just happened: each builder picks a status, and the finisher decides the body. Use .json() when you have a serializable value, .body() for plain text or raw bytes, and .finish() when the status is the whole message (a 204 or a bare 404 carries no payload). There are named helpers for the common codes; for anything exotic you can reach for HttpResponse::build(StatusCode::IM_A_TEAPOT) and build from a raw status.
💡 A useful instinct: when you find yourself wanting to control the status code, you've found the moment to use
HttpResponse. The other return types are conveniences that pin the status for you — great until you need to say201or404.
web::Json: the shorthand
If your handler always returns a 200 with a JSON body, the HttpResponse::Ok().json(…) dance 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 ;
async
What just happened: web::Json(article) is a responder that serializes its inner value and responds with 200 OK — exactly what HttpResponse::Ok().json(&article) does, with less typing. Note you hand it the value by ownership (article), not by reference, since the wrapper takes it over.
You met web::Json in Phase 2 as an extractor — it pulled a JSON body out of the request. The same type works in both directions: as a parameter it's input, as a return value it's output. Same wrapper, opposite ends of the handler.
⚠️ The tradeoff is real:
web::Jsonalways responds with 200. The moment you need a201 Createdafter a POST, or a404when the article doesn't exist,web::Jsoncan't express it — you have to go back toHttpResponse::Ok().json(...)/HttpResponse::Created().json(...)to choose the status. So reach forweb::Jsonon the read paths where 200 is genuinely always correct, and useHttpResponseeverywhere the status varies.
The trap: different branches, different types
This is the one that bites everyone exactly once. You write a handler that returns a 200 when it finds the article and a 404 when it doesn't, and the compiler refuses to build it:
use ;
// ⚠️ This does NOT compile.
async
What just happened: the two branches return different concrete types — one is an HttpResponse, the other is a web::Json<Article>. impl Responder means "some single type that implements Responder," and a Rust function can only return one concrete type. Two different types from two branches isn't allowed, even though both implement Responder. The compiler error talks about "expected HttpResponse, found Json<Article>," which is its way of saying "pick one type."
The fix is to make every branch produce the same concrete type. The easiest choice is HttpResponse for both, since it can represent any status:
use ;
async
What just happened: both branches now return HttpResponse, so the function has a single, consistent return type and the compiler is happy. The .finish() path and the .json() path are both HttpResponse — the status and body differ, but the type is identical, and that's all Rust cares about.
💡 The rule of thumb: the moment a handler can return more than one status, return
HttpResponsefrom every branch. Saveweb::Jsonand bareimpl Responderfor handlers with exactly one outcome shape. (There's an even cleaner way to vary status — returning aResultand letting theResponseErrortrait map errors to status codes. That's Phase 6. For now, a singleHttpResponsetype in branchy handlers is the honest, working answer.)
impl Responder vs HttpResponse: which to write
You've now seen both in the wild, so here's how to choose between them.
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. You let the type be implied and keep the signature short.
HttpResponse is the explicit, flexible choice. Write it when you need control over the status, when different branches must agree on a type (the trap above), or when you want the signature to state plainly "this returns an HTTP response."
use ;
// Single shape, terse: impl Responder is a fine fit.
async
// Status varies / branches: be explicit with HttpResponse.
async
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 because the return type is already the concrete HttpResponse, both branches line up with no fuss. Both signatures are valid; the difference is whether you want flexibility (name HttpResponse) or brevity (return impl Responder for a single shape).
📝 You may have noticed strings work too: returning a
&'static strorStringfrom a handler sends it as a200 OKtext body — they implementResponderas well. Handy for a quick health-check route, rarely what you want for a real API. The articles API speaks JSON, soHttpResponseandweb::Jsonare 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. Your job is to return the right type. HttpResponseis the workhorse: a builder with status helpers (Ok,Created,NotFound,NoContent,BadRequest) finished by.json(&value),.body("…"), or.finish(). Reach for it whenever you need to control the status.web::Json(value)is the shorthand for "200 + JSON body" — terse, but locked to status 200. The same wrapper is an extractor on input and a responder on output.- Different branches must return the same concrete type. Mixing
HttpResponseandweb::Jsonacrossifbranches won't compile; use oneHttpResponsetype everywhere a handler can vary its status (or wait for Phase 6'sResult/ResponseError). - Choose
impl Responderfor single-shape, terse handlers; chooseHttpResponsefor 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."
}
]
← Phase 2: Routing & Extractors · Guide overview · Phase 4: Shared State with web::Data →
Check your understanding
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?