← Back to Home
Node.js

Building a REST API with Node.js and Express

Kuligaposten

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: 201 for a successful creation, 204 (no body) for a successful delete, 404 when the resource doesn’t exist, 400 for a bad request body.
  • Always validate req.params.id against what you’re comparing it to — route params come through as strings, so req.params.id === note.id would never match without the Number(...) conversion.
  • Return early on error checks — if (!note) return res.status(404)... avoids a pyramid of else blocks.

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.

Related Topics

← Back to Home