Skip to content
Laviyx Learn

Try

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.

Intermediate8 min readUpdated
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#

MethodSends
res.json(data)JSON, with Content-Type: application/json
res.send(body)text, HTML or a Buffer
res.status(code)sets the status (chain it: res.status(201).json(...))
res.sendStatus(code)only a status, with its standard text as the body
res.end()ends the response with no body
res.set(name, value)sets a header
res.redirect(url)redirects (302 by default)
res.download(path)sends a file as a download

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.

statuses.js (JavaScript)
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);
Terminal
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
Output
{"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#

headers.js (JavaScript)
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);
Terminal
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)"
Output
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.csv

A consistent response shape#

Clients are easier to write when every endpoint answers in the same shape. A common convention:

JSON
{ "data": { "id": 7, "name": "Backpack" } }

for success, and for errors:

JSON
{ "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
patch.js (JavaScript)
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);
Terminal
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"}'
Output
{"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?