Building a REST API with Node.js and Express
Building a REST API with Node.js and Express
Express is still the most common way to stand up an HTTP API in Node — minimal, unopinionated, and well understood. This walks through building a small CRUD API for a single resource (notes), the way you’d actually structure it, not just a one-file toy example.
Project setup
mkdir notes-api && cd notes-api
npm init -y
npm install express
Add "type": "module" to package.json so we can use ES module imports.
A minimal server
// server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.json({ status: 'ok' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
express.json() is the built-in body parser middleware — without it, req.body on a POST/PUT request would be undefined.
In-memory data store
For a real app this would be a database, but an array is enough to see the routing pattern clearly:
let notes = [
{ id: 1, title: 'First note', body: 'Hello world' },
];
let nextId = 2;
CRUD routes
// GET /notes - list all
app.get('/notes', (req, res) => {
res.json(notes);
});
// GET /notes/:id - get one
app.get('/notes/:id', (req, res) => {
const note = notes.find((n) => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: 'Note not found' });
res.json(note);
});
// POST /notes - create
app.post('/notes', (req, res) => {
const { title, body } = req.body;
if (!title) return res.status(400).json({ error: 'title is required' });
const note = { id: nextId++, title, body: body ?? '' };
notes.push(note);
res.status(201).json(note);
});
// PUT /notes/:id - update
app.put('/notes/:id', (req, res) => {
const note = notes.find((n) => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: 'Note not found' });
const { title, body } = req.body;
if (title !== undefined) note.title = title;
if (body !== undefined) note.body = body;
res.json(note);
});
// DELETE /notes/:id - remove
app.delete('/notes/:id', (req, res) => {
const index = notes.findIndex((n) => n.id === Number(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Note not found' });
notes.splice(index, 1);
res.status(204).send();
});
A few things worth noticing:
- Status codes matter:
201for a successful creation,204(no body) for a successful delete,404when the resource doesn’t exist,400for a bad request body. - Always validate
req.params.idagainst what you’re comparing it to — route params come through as strings, soreq.params.id === note.idwould never match without theNumber(...)conversion. - Return early on error checks —
if (!note) return res.status(404)...avoids a pyramid ofelseblocks.
Centralized error handling
Instead of a try/catch in every route, Express supports a dedicated error-handling middleware — a function with four parameters (err, req, res, next), placed after all your routes:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Something went wrong' });
});
Any route can trigger it by calling next(err):
app.get('/notes/:id', (req, res, next) => {
try {
const note = notes.find((n) => n.id === Number(req.params.id));
if (!note) return res.status(404).json({ error: 'Note not found' });
res.json(note);
} catch (err) {
next(err);
}
});
For synchronous code this is optional (Express catches synchronous throws automatically), but for anything async — a database call, an external API request — you need to catch the rejection yourself and pass it to next, since Express won’t do that for you.
Testing it
curl http://localhost:3000/notes
curl -X POST http://localhost:3000/notes \
-H "Content-Type: application/json" \
-d '{"title":"New note","body":"Some content"}'
That’s the whole shape of a small REST API: a router per resource, consistent status codes, and one error handler at the bottom instead of scattered try/catch blocks. From here the natural next steps are swapping the in-memory array for a real database and splitting the routes into their own file as the app grows.
