Node.js and Express course menu
Express Responses: res.json, Status Codes and Headers
Send the right response from Express: res.json, res.send, res.status, res.sendStatus and res.end, choosing HTTP status codes (200, 201, 204, 400, 401, 403, 404, 409, 422, 500), setting headers, redirects, downloads, and a consistent JSON response shape for APIs.
On this page
Every request gets exactly one response: a status code, headers and usually a body. Choosing them carefully makes an API predictable for the developers who use it.
Response methods#
Status codes#
Group by the first digit:
- 2xx success:
200 OK,201 Created(with the new resource),204 No Content(success, nothing to return, e.g. after a delete). - 3xx redirection:
301 Moved Permanently,302 Found,304 Not Modified. - 4xx client errors (the request was wrong):
400 Bad Request(invalid input),401 Unauthorized(not logged in),403 Forbidden(logged in but not allowed),404 Not Found,409 Conflict(for example, email already registered),422 Unprocessable Content(well-formed but semantically invalid; some APIs use it for validation errors),429 Too Many Requests. - 5xx server errors (your code failed):
500 Internal Server Error,503 Service Unavailable.
The rule of thumb: 4xx means the client should change the request; 5xx means the server has a problem.
import express from "express";
const app = express();
app.use(express.json());
const users = [{ id: 1, email: "asha@example.com" }];
app.post("/api/users", (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).json({ error: "email is required" });
if (users.some((u) => u.email === email)) {
return res.status(409).json({ error: "An account with this email already exists" });
}
const user = { id: users.length + 1, email };
users.push(user);
res.status(201).location(`/api/users/${user.id}`).json(user);
});
app.delete("/api/users/:id", (req, res) => {
const index = users.findIndex((u) => u.id === Number(req.params.id));
if (index === -1) return res.sendStatus(404);
users.splice(index, 1);
res.sendStatus(204);
});
app.listen(3000);curl -s -w " [%{http_code}]\n" -X POST localhost:3000/api/users -H "Content-Type: application/json" -d '{"email":"ravi@example.com"}'
curl -s -w " [%{http_code}]\n" -X POST localhost:3000/api/users -H "Content-Type: application/json" -d '{"email":"asha@example.com"}'
curl -s -w " [%{http_code}]\n" -X POST localhost:3000/api/users -H "Content-Type: application/json" -d '{}'
curl -s -w " [%{http_code}]\n" -X DELETE localhost:3000/api/users/2
curl -s -w " [%{http_code}]\n" -X DELETE localhost:3000/api/users/2{"id":2,"email":"ravi@example.com"} [201]
{"error":"An account with this email already exists"} [409]
{"error":"email is required"} [400]
[204]
Not Found [404]-w " [%{http_code}]\n" prints the status after each body. Note that sendStatus(404) sends the text "Not Found", and 204 has no body at all. res.location() sets the Location header pointing to the new resource, a REST convention for 201 responses.
Headers#
import express from "express";
const app = express();
app.get("/api/prices", (req, res) => {
res.set("Cache-Control", "public, max-age=300"); // browsers may cache for 5 minutes
res.set("X-Data-Source", "price-service");
res.json({ notebook: 60, pen: 25 });
});
app.get("/old-shop", (req, res) => res.redirect(301, "/shop"));
app.get("/api/report.csv", (req, res) => {
res.attachment("report.csv"); // download with this file name
res.type("text/csv").send("product,sold\nNotebook,120\nPen,340\n");
});
app.listen(3000);curl -s -i localhost:3000/api/prices | grep -E "^(HTTP|Cache-Control|X-Data-Source|Content-Type)"
curl -s -i localhost:3000/old-shop | grep -E "^(HTTP|Location)"
curl -s -i localhost:3000/api/report.csv | grep -E "^(HTTP|Content-Disposition|Content-Type)"HTTP/1.1 200 OK
Cache-Control: public, max-age=300
X-Data-Source: price-service
Content-Type: application/json; charset=utf-8
HTTP/1.1 301 Moved Permanently
Location: /shop
HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename=report.csvA consistent response shape#
Clients are easier to write when every endpoint answers in the same shape. A common convention:
{ "data": { "id": 7, "name": "Backpack" } }for success, and for errors:
{ "error": { "code": "VALIDATION_FAILED", "message": "price must be positive", "details": [] } }with the HTTP status code still set correctly. A machine-readable code lets front ends react to specific errors without parsing messages. Whatever shape you choose, document it and use it everywhere.
Common mistakes#
Practice#
Practice exercise
Add PATCH /api/users/:id to the users example that changes the email: 404 if the user does not exist, 400 without an email, 409 if another user already has that email, otherwise 200 with the updated user.
Show hint
Find the user first, then check users.some((u) => u.email === email && u.id !== user.id) for conflicts.
Show solution
import express from "express";
const app = express();
app.use(express.json());
const users = [{ id: 1, email: "asha@example.com" }, { id: 2, email: "ravi@example.com" }];
app.patch("/api/users/:id", (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: "User not found" });
const { email } = req.body;
if (!email) return res.status(400).json({ error: "email is required" });
if (users.some((u) => u.email === email && u.id !== user.id)) {
return res.status(409).json({ error: "Email already in use" });
}
user.email = email;
res.json(user);
});
app.listen(3000);curl -s -w " [%{http_code}]\n" -X PATCH localhost:3000/api/users/2 -H "Content-Type: application/json" -d '{"email":"ravi.k@example.com"}'
curl -s -w " [%{http_code}]\n" -X PATCH localhost:3000/api/users/2 -H "Content-Type: application/json" -d '{"email":"asha@example.com"}'
curl -s -w " [%{http_code}]\n" -X PATCH localhost:3000/api/users/9 -H "Content-Type: application/json" -d '{"email":"x@example.com"}'{"id":2,"email":"ravi.k@example.com"} [200]
{"error":"Email already in use"} [409]
{"error":"User not found"} [404]Key takeaway#
Next: Error Handling.
Finished this lesson?