JSON Responses and HTTP Status Codes
Objectives
By the end of this lesson, you should be able to:
- Send JSON responses with
res.json() - Choose the correct HTTP status code for a given outcome
- Combine status codes and JSON to signal success and failure clearly
💡 Why this matters: An API client (a frontend app, another service) relies on the status code to know whether a request succeeded, without ever reading the response body. Getting this right is what separates a working API from one that technically returns data but is unpleasant and unreliable to build against.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.
A Small Books API
const express = require('express');
const app = express();
app.use(express.json());
const books = [
{ id: 1, title: 'The Pragmatic Programmer' },
{ id: 2, title: 'Clean Code' }
];
app.get('/books', (req, res) => {
res.status(200).json(books);
});
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
return res.status(404).json({ error: 'Book not found' });
}
res.status(200).json(book);
});
app.post('/books', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'title is required' });
}
const newBook = { id: books.length + 1, title };
books.push(newBook);
res.status(201).json(newBook);
});
app.listen(4006);
curl -i http://localhost:4006/books/99
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
{"error":"Book not found"}
curl -i -X POST http://localhost:4006/books -H "Content-Type: application/json" -d '{"title":"Refactoring"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"id":3,"title":"Refactoring"}
curl -i -X POST http://localhost:4006/books -H "Content-Type: application/json" -d '{}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"error":"title is required"}
Every response here pairs a status code with a JSON body that matches it, 200 with the requested data, 404 with an error explaining what wasn’t found, 201 with the newly created resource, 400 with an error explaining what was wrong with the request. The return before res.status(404).json(...) matters, without it, execution would continue to the line below and try to send a second response, which throws an error.
Common Status Codes to Know
200 OK, a successful request with a body to return. 201 Created, a resource was successfully created (typically after POST). 400 Bad Request, the client sent something wrong, a missing field, invalid data. 404 Not Found, the requested resource doesn’t exist. 500 Internal Server Error, something failed on the server’s side, not the client’s fault (Module 8 covers this in depth). Status codes are grouped by their first digit: 2xx means success, 4xx means the client made a mistake, 5xx means the server did.
Try It
- Add a
PUT /books/:idroute updating a book’s title, returning200with the updated book if found, or404if not. - Add a
DELETE /books/:idroute removing a book, returning200with a confirmation message if found, or404if not. - Add validation to the
PUTroute rejecting an emptytitlewith400, matching thePOSTroute’s validation. - Explain, in your own words, why an API returning
200 OKwith a body like{"error": "not found"}is worse design than returning404with the same body.
Recap
res.status(code).json(data)is the standard pattern, pairing a status code with a matching JSON body.- 2xx means success, 4xx means the client’s request was wrong, 5xx means the server failed.
returnbefore sending an error response prevents execution from continuing to send a second, conflicting response.
Next lesson: this module’s exercises, building a small multi-route Express API.