Building a REST API
Learn the REST conventions that power the web, then build a real in-memory CRUD API with Express — resources, HTTP verbs, status codes, and JSON all included.
Almost every app you use daily — weather apps, social feeds, shopping carts — is secretly two programs talking to each other over the internet. The frontend (what you see) sends requests; the backend (the server) replies with data. The agreed-upon language for that conversation is usually a REST API. Once you understand REST, you can build the backend half of any web application. In this lesson you will go from zero to a working, in-memory API that can create, read, update, and delete items — the four fundamental operations of nearly every data-driven app.
#What is REST?
REST stands for Representational State Transfer — a set of conventions (not a strict standard) for designing web APIs. Three ideas make up the heart of it:
- Resources — Everything the API exposes is a "resource" (a book, a user, an order). Each resource lives at a URL.
- HTTP verbs — The type of action you want is encoded in the HTTP method: GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
- Stateless requests — Each request must contain all the information the server needs. The server does not remember what you asked last time.
That's it. REST is not a library or a framework; it is a style.
A REST API is like a post office
Imagine every type of item in a warehouse has its own address (URL). You send a letter (HTTP request) to that address. The envelope color tells the warehouse worker what you want: a blue envelope means "send me the item" (GET), a green envelope means "here is a new item, please store it" (POST), a yellow envelope means "replace this item" (PUT), and a red envelope means "throw this item away" (DELETE). The worker sends back a reply slip (the HTTP response) confirming what happened — or explaining what went wrong.
#HTTP Verbs at a Glance
For a resource like /books, here is the conventional mapping of verbs to actions:
| Method | URL | What it does | |--------|-----|--------------| | GET | /books | Return all books | | GET | /books/42 | Return book with id 42 | | POST | /books | Create a new book | | PUT | /books/42 | Replace book 42 entirely | | DELETE | /books/42 | Delete book 42 |
This naming convention is not enforced by any technology — it is a social contract that makes APIs predictable for every developer who uses them.
#Status Codes: the Server's Emoji
Every HTTP response carries a status code — a three-digit number that tells the client at a glance whether things went well. You do not need to memorize them all, but a handful appear in every API:
- 200 OK — the request succeeded
- 201 Created — a new resource was created (use this after a successful POST)
- 400 Bad Request — the client sent something the server could not understand
- 404 Not Found — the resource does not exist
- 500 Internal Server Error — something broke on the server side
Choosing the right status code is part of building a polite API. A client should be able to know whether its request worked without parsing the body.
201 vs 200 after a POST
When you create something new, respond with 201 Created, not 200 OK. It is a small detail, but it lets the caller know exactly what happened — and some HTTP clients and proxies treat 201 differently from 200. It costs one extra character and signals intent clearly.
#Setting Up Express
Express is the most popular Node.js framework for building APIs. It handles the plumbing — routing, parsing request bodies, sending responses — so you can focus on your logic. Start a new project and install it:
mkdir books-api && cd books-api
npm init -y
npm install express#Building the API: GET and POST
Here is a complete, working file for an in-memory books API. "In-memory" means the data lives in a JavaScript array — it resets when the server restarts, which is perfect for learning because there is no database to set up.
// index.js
const express = require("express");
const app = express();
// Tell Express to parse JSON request bodies automatically
app.use(express.json());
// In-memory "database"
let books = [
{ id: 1, title: "The Pragmatic Programmer", author: "Hunt & Thomas" },
{ id: 2, title: "You Don't Know JS", author: "Kyle Simpson" }
];
let nextId = 3;
// GET /books — return all books
app.get("/books", (req, res) => {
res.json(books);
});
// GET /books/:id — return one book
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.json(book);
});
// POST /books — create a new book
app.post("/books", (req, res) => {
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
const newBook = { id: nextId++, title, author };
books.push(newBook);
res.status(201).json(newBook);
});
app.listen(3000, () => console.log("API running on http://localhost:3000"));Forgetting express.json() is a classic first bug
Without app.use(express.json()), req.body is always undefined — even when the client sends a perfectly valid JSON body. This trips up almost every beginner. Always add express.json() before your routes if you expect a JSON body.
#Adding PUT and DELETE
// PUT /books/:id — replace a book
app.put("/books/:id", (req, res) => {
const index = books.findIndex(b => b.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: "Book not found" });
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
books[index] = { id: Number(req.params.id), title, author };
res.json(books[index]);
});
// DELETE /books/:id — remove a book
app.delete("/books/:id", (req, res) => {
const index = books.findIndex(b => b.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: "Book not found" });
books.splice(index, 1);
res.status(200).json({ message: "Book deleted" });
});#Testing Your API
With the server running (node index.js), you can test every endpoint with curl in a second terminal — no browser or extra tool needed.
# Get all books
curl http://localhost:3000/books
# Create a new book
curl -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-d '{"title": "Clean Code", "author": "Robert Martin"}'
# Get book with id 3
curl http://localhost:3000/books/3
# Delete book with id 1
curl -X DELETE http://localhost:3000/books/1You send a POST request to create a new resource, and the server responds with status code 200. What is the problem?
Key takeaways
- REST maps HTTP verbs (GET, POST, PUT, DELETE) onto CRUD actions on named resources (URLs).
- Status codes communicate the result at a glance: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Server Error.
- Always add `app.use(express.json())` before your routes, or `req.body` will be undefined.
- An in-memory array is a great zero-setup database for learning; swap it for a real database when you're ready.
- Test any API quickly with `curl` from the terminal — no extra tools needed.
This POST route from the books API runs when a client sends a valid JSON body of {"title": "Clean Code", "author": "Robert Martin"}. What status code does the server send back?
app.post("/books", (req, res) => {
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
const newBook = { id: nextId++, title, author };
books.push(newBook);
res.status(201).json(newBook);
});This POST route reads req.body but title and author always come back undefined, even when the client sends valid JSON. What's wrong?
const express = require("express");
const app = express();
app.post("/books", (req, res) => {
const { title, author } = req.body;
const newBook = { id: nextId++, title, author };
books.push(newBook);
res.status(201).json(newBook);
});
app.listen(3000);Complete this GET route so it returns 404 when the book id doesn't exist. Fill in the HTTP status code for 'Not Found'.
app.get("/books/:id", (req, res) => { const book = books.find(b => b.id === Number(req.params.id)); if (!book) return res.status().json({ error: "Book not found" }); res.json(book); });
Put these lines in the correct order to build a minimal Express books API that parses JSON, serves GET /books, and starts listening.
app.listen(3000, () => console.log("API running"));app.use(express.json());
const app = express();
const express = require("express");app.get("/books", (req, res) => res.json(books));Extend the books API with a PATCH /books/:id endpoint that lets callers update only the fields they provide. If only 'title' is sent, only the title should change; if only 'author' is sent, only the author should change. Return 404 if the book does not exist, and 400 if neither field is provided.
Try it yourself — a starting point to build on:
// Assume 'books' array and Express app already exist from the lesson.
// TODO: Add a PATCH /books/:id route
app.patch("/books/:id", (req, res) => {
// TODO: Find the book by id (remember to convert req.params.id to a Number)
const index = /* find the book's index */;
// TODO: Return 404 if not found
// TODO: Destructure title and author from req.body
// TODO: Return 400 if neither field was provided
// TODO: Update only the fields that were provided (leave the rest unchanged)
// TODO: Respond with the updated book
});