REST APIs with Express
Reviewed & published by Brayan K
A REST API exposes resources at URLs and uses HTTP methods — GET, POST, PUT, DELETE — to read and change them, returning JSON with meaningful status codes.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll build a full CRUD API in Express: listing and fetching items, creating with POST, updating with PUT, deleting with DELETE, and returning the right status codes throughout.
What You'll Learn in This Lesson
- What REST is: resources, URLs, and HTTP methods
- Reading collections and single items with GET
- Using URL parameters via req.params
- Creating resources with POST and req.body
- Updating with PUT and deleting with DELETE
- Returning correct status codes (200, 201, 204, 404)
1️⃣ Reading Data with GET
The simplest REST endpoints read data. GET /books returns the whole collection; GET /books/:id returns one item, where :id is a URL parameter available as req.params.id. If the item doesn't exist, return 404.
// A REST API exposes RESOURCES (like "books") at URLs, and uses HTTP
// METHODS to say what to do: GET=read, POST=create, PUT=update, DELETE=remove.
const express = require("express");
const app = express();
app.use(express.json()); // so we can read JSON request bodies later
// Our "database" is just an in-memory array for this lesson.
let books = [
{ id: 1, title: "Dune" },
{ id: 2, title: "1984" },
];
// GET /books -> return the whole collection.
app.get("/books", (req, res) => {
res.json(books);
});
// GET /books/:id -> return ONE book. ":id" is a URL parameter.
app.get("/books/:id", (req, res) => {
// req.params.id is always a string, so convert it with Number().
const book = books.find((b) => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: "Not found" });
res.json(book);
});
app.listen(3000);
// GET /books -> 200 [{"id":1,...},{"id":2,...}]
// GET /books/2 -> 200 {"id":2,"title":"1984"}
// GET /books/99 -> 404 {"error":"Not found"}Note the Number(req.params.id) conversion — URL parameters are always strings, so comparing a string id to a numeric id silently fails without it.
2️⃣ Creating Data with POST
POST creates a new resource. The client sends data in the request body as JSON; express.json() parses it onto req.body. Validate the input, create the item, then respond with 201 Created and the new resource.
// POST creates a new resource. The data arrives in the REQUEST BODY as JSON,
// which express.json() parses onto req.body.
const express = require("express");
const app = express();
app.use(express.json());
let books = [{ id: 1, title: "Dune" }];
let nextId = 2;
app.post("/books", (req, res) => {
// Validate input first — never trust the client.
if (!req.body.title) {
return res.status(400).json({ error: "title is required" });
}
const book = { id: nextId++, title: req.body.title };
books.push(book);
// 201 Created is the correct status for a successful POST.
// It's polite to return the newly created resource.
res.status(201).json(book);
});
app.listen(3000);
// POST /books body {"title":"Hyperion"}
// -> 201 {"id":2,"title":"Hyperion"}
// POST /books body {}
// -> 400 {"error":"title is required"}3️⃣ Updating and Deleting
PUT /books/:id updates an existing item and returns it with 200. DELETE /books/:id removes one and returns 204 No Content — success with an empty body. Both return 404 when the id isn't found.
// PUT updates an existing resource; DELETE removes one.
const express = require("express");
const app = express();
app.use(express.json());
let books = [
{ id: 1, title: "Dune" },
{ id: 2, title: "1984" },
];
// PUT /books/:id -> replace the title of one book.
app.put("/books/:id", (req, res) => {
const book = books.find((b) => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: "Not found" });
book.title = req.body.title;
res.json(book); // 200 with the updated resource
});
// DELETE /books/:id -> remove one book.
app.delete("/books/:id", (req, res) => {
const id = Number(req.params.id);
const exists = books.some((b) => b.id === id);
if (!exists) return res.status(404).json({ error: "Not found" });
books = books.filter((b) => b.id !== id);
res.status(204).end(); // 204 No Content: success, nothing to return
});
app.listen(3000);
// PUT /books/1 body {"title":"Dune (new)"} -> 200 {"id":1,"title":"Dune (new)"}
// DELETE /books/2 -> 204 (empty body)Your turn. Fill in the two ___ blanks to finish a tiny todos API, then run it.
// 🎯 YOUR TURN — finish the two routes, then run: node api.js
const express = require("express");
const app = express();
app.use(express.json());
let todos = [{ id: 1, text: "Learn REST" }];
let nextId = 2;
// TODO 1: GET /todos should return the whole list as JSON.
app.get("/todos", (req, res) => {
res.json(___); // 👉 the todos array
});
// TODO 2: POST /todos should create a todo from req.body.text,
// push it, and respond with status 201 and the new todo.
app.post("/todos", (req, res) => {
const todo = { id: nextId++, text: req.body.text };
todos.push(todo);
res.status(___).json(todo); // 👉 the "Created" status code
});
app.listen(3000, () => console.log("API on 3000"));
// ✅ After POST /todos {"text":"Ship it"}:
// GET /todos -> [{"id":1,...},{"id":2,"text":"Ship it"}]Mini-Challenge: Full CRUD for Notes
Build all four operations for a notes resource: list, fetch one, update, and delete — with correct status codes and 404s. Run it with node notes.js.
// 🎯 MINI-CHALLENGE: complete the CRUD for a "notes" resource.
// Save as notes.js and run: node notes.js
//
// Starting data: let notes = [{ id: 1, body: "first note" }];
//
// Implement all four:
// GET /notes -> 200 list of notes
// GET /notes/:id -> 200 one note, or 404 if missing
// PUT /notes/:id -> 200 updated note (set body from req.body.body)
// DELETE /notes/:id -> 204 on success, 404 if missing
//
// Remember: req.params.id is a string — convert with Number().
//
// ✅ Example:
// GET /notes/1 -> {"id":1,"body":"first note"}
const express = require("express");
const app = express();
app.use(express.json());
let notes = [{ id: 1, body: "first note" }];
// your code here
app.listen(3000);Common Errors (and the fix)
- Lookup always returns 404 — You compared a string req.params.id to a numeric id. Convert with Number(req.params.id) first.
- req.body is undefined — Missing app.use(express.json()). Add it before your routes so JSON bodies are parsed.
- "Cannot set headers after they are sent" — You forgot to return after an early res.status(404).json(...), so the code kept running and responded twice.
- POST returns 200 instead of 201 — Express defaults to 200. Set res.status(201) explicitly when you create something.
- DELETE returns an empty 200 — Prefer res.status(204).end() to signal "success, no content" clearly.
Pro Tips
- 💡 Validate early, return early: check required fields at the top and return res.status(400)... before touching your data.
- 💡 Name resources as plural nouns: /books, not /getBooks. The HTTP method already says the verb.
- 💡 Always return the created/updated item: it saves the client a second request to fetch it.
📋 Quick Reference — REST with Express
| Action | Route + status |
|---|---|
| List all | GET /books → 200 |
| Get one | GET /books/:id → 200 / 404 |
| Create | POST /books → 201 |
| Update | PUT /books/:id → 200 |
| Delete | DELETE /books/:id → 204 |
| Bad input | → 400 |
🎉 Lesson Complete!
- ✅ REST maps resources to URLs and actions to HTTP methods
- ✅ GET reads, POST creates, PUT updates, DELETE removes
- ✅ req.params holds URL parts; convert ids with Number()
- ✅ req.body holds the parsed JSON body (needs express.json())
- ✅ Return the right status: 200, 201, 204, 400, 404
Practice quiz
In REST, which HTTP method is used to read a resource?
- GET
- POST
- DELETE
- PUT
Answer: GET. GET reads data; POST creates, PUT updates, and DELETE removes.
Which method creates a new resource?
- GET
- POST
- PUT
- DELETE
Answer: POST. POST creates a new resource from the data in the request body.
Why must you wrap req.params.id with Number() before comparing to a numeric id?
- To round it
- To make it negative
- Because params are strings and '3' === 3 is false
- To encrypt it
Answer: Because params are strings and '3' === 3 is false. URL params arrive as strings, so a strict comparison to a numeric id fails without conversion.
What status code should a successful POST that creates a resource return?
- 200 OK
- 404 Not Found
- 204 No Content
- 201 Created
Answer: 201 Created. 201 Created is the correct status after successfully creating a new resource.
Which middleware lets you read a JSON request body on req.body?
- express.json()
- express.static()
- express.raw()
- express.urlencoded()
Answer: express.json(). app.use(express.json()) parses JSON bodies and populates req.body.
What status code is appropriate for a successful DELETE with no body to return?
- 201 Created
- 204 No Content
- 200 OK
- 400 Bad Request
Answer: 204 No Content. 204 No Content signals success with an empty response body.
Which status code means the client sent invalid data, like a missing required field?
- 404 Not Found
- 500 Server Error
- 400 Bad Request
- 201 Created
Answer: 400 Bad Request. 400 Bad Request indicates the client's input was invalid.
What does req.params.id refer to in the route GET /books/:id?
- The query string
- The request body
- A response header
- The URL parameter value for :id
Answer: The URL parameter value for :id. req.params.id holds the value matched by the :id segment of the URL.
Why does 'Cannot set headers after they are sent' often occur in a route?
- You forgot to return after an early res.json
- The port is wrong
- express.json() is missing
- The database is down
Answer: You forgot to return after an early res.json. Without returning after an early response, code keeps running and responds twice.
Which status code means the requested resource does not exist?
- 200
- 204
- 404
- 201
Answer: 404. 404 Not Found is returned when the requested resource cannot be found.
Continue this course
- Previous: Middleware
- Next: Connecting a Database — Persist data by connecting your API to a real database
- Quick reference: Node.js cheat sheet
Frequently asked questions
What makes an API RESTful?
REST is a style for designing web APIs around resources and the standard HTTP methods. Each resource (like a book or a user) lives at a URL such as /books or /books/3, and you act on it with the method that matches your intent: GET to read, POST to create, PUT or PATCH to update, and DELETE to remove. Responses use meaningful HTTP status codes (200, 201, 404, and so on) and usually carry JSON. Following these conventions makes your API predictable: anyone who knows REST can guess how to use it.
Why is req.params.id a string, and why does my comparison fail?
Everything in a URL is text, so Express gives you route parameters as strings — req.params.id is "3", not the number 3. If your data stores numeric ids, a strict comparison like b.id === req.params.id fails because "3" === 3 is false. Convert the parameter with Number(req.params.id) (or parseInt) before comparing. This single mismatch is one of the most common reasons a lookup returns 404 even though the record clearly exists.
Which status codes should I return?
Use 200 OK for a successful GET, PUT, or any request that returns data; 201 Created after a successful POST that makes a new resource (and return the created item); 204 No Content for a successful DELETE that returns nothing; 400 Bad Request when the client sent invalid data, like a missing required field; 404 Not Found when the requested resource doesn't exist; and 500 for unexpected server errors. Returning the right code lets clients react correctly without parsing your error messages.
How do I read the data a client sends in a POST?
For JSON, register express.json() as middleware once with app.use(express.json()), and the parsed body appears on req.body. So a POST with body {"title":"Dune"} gives you req.body.title === "Dune". Without that middleware, req.body is undefined and you'll be confused why your data is missing. Always validate what you receive — check that required fields exist and have the right type — before storing it, because clients can send anything.