Most backends you'll build are a REST API: a set of URLs that return JSON instead of HTML. A Discord bot's dashboard, a mobile app's server, a public data feed — all the same shape. This guide covers the three things that separate a tidy API from a messy one: routes that read well, status codes that tell the truth, and validation that rejects bad input before it reaches your logic. It runs on the Node.js application with Express.
| At a glance | |
|---|---|
| You need | A server running the Node.js application |
| Background | Your first web app (the SERVER_PORT / 0.0.0.0 rule) and Build an Express website + API |
| Plan | Any plan |
| Time | Thirty minutes |
New to Express here? Read Build an Express website + API first — this guide assumes the index.js entry file and automatic npm install it explains.
Design the routes first
A REST API maps things (resources) to URLs, and actions to HTTP methods. Pick a plural noun for the resource and let the method carry the verb — never put the verb in the URL.
| Do this | Not this |
|---|---|
GET /api/notes |
GET /api/getAllNotes |
GET /api/notes/42 |
GET /api/note?id=42&action=fetch |
POST /api/notes |
POST /api/createNote |
DELETE /api/notes/42 |
POST /api/deleteNote |
The pattern is always method + collection + optional id. Read it aloud: "GET notes" lists them, "POST notes" adds one, "DELETE notes 42" removes one. Once the shape is consistent, anyone can guess the rest.
Status codes that tell the truth
The status code is the first thing a client reads. Sending 200 OK on a failure is the single most common API bug — it forces every caller to dig through the body to find out what really happened. Use the small set that covers almost everything:
| Code | Means | Use it when |
|---|---|---|
200 OK |
Success, here's the data | A GET that found something |
201 Created |
Made a new thing | A POST that added a row |
204 No Content |
Success, nothing to return | A DELETE that worked |
400 Bad Request |
Your request was malformed | Missing/garbage input |
401 Unauthorized |
You're not logged in | No/invalid credentials — see Sessions, JWT & passwords |
404 Not Found |
That thing doesn't exist | An id that isn't there |
422 Unprocessable Entity |
Valid shape, invalid values | Validation failed |
429 Too Many Requests |
Slow down | Rate limited — see Rate limiting your API |
500 Internal Server Error |
You broke, not them | An unhandled exception |
Validate before you trust
Never write a request body straight into your data. Check it first, and reject bad input with a clear message and a 4xx code. The express-validator library does this cleanly, but the principle is the same if you hand-roll the checks: validate, and only then act.
Install express, then express-validator, from the Packages page (search each name, install, then restart). Here's a complete index.js — a notes API with all four routes, honest status codes, and validation on the one route that takes input:
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json()); // parse JSON request bodies
let notes = [{ id: 1, title: 'First note', body: 'hello' }];
let nextId = 2;
// List all
app.get('/api/notes', (req, res) => {
res.json(notes);
});
// Get one — 404 if it isn't there
app.get('/api/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);
});
// Create one — validate, then 201
app.post('/api/notes',
body('title').isString().trim().notEmpty().withMessage('title is required'),
body('body').isString().trim().notEmpty().withMessage('body is required'),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
const note = { id: nextId++, title: req.body.title, body: req.body.body };
notes.push(note);
res.status(201).json(note);
}
);
// Delete one — 204 on success, 404 if missing
app.delete('/api/notes/:id', (req, res) => {
const idx = notes.findIndex((n) => n.id === Number(req.params.id));
if (idx === -1) return res.status(404).json({ error: 'Note not found' });
notes.splice(idx, 1);
res.status(204).end();
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
express.json() turns the incoming JSON body into req.body. Each body('title')... line is a rule; validationResult(req) collects any failures so you can return them all at once with a 422. Every route ends in exactly one response.
Verify it works
Start the server, then hit it from the Console's neighbour — your own machine — using your server's public address (Network page). The shape you should see:
| Request | Response |
|---|---|
GET /api/notes |
200 + the array |
GET /api/notes/999 |
404 + {"error":"Note not found"} |
POST /api/notes with {"title":"Hi","body":"there"} |
201 + the new note |
POST /api/notes with {"title":""} |
422 + the validation errors |
DELETE /api/notes/1 |
204, empty body |
From a terminal, curl proves it in one line each:
curl -i -X POST -H "Content-Type: application/json" \
-d '{"title":"Hi","body":"there"}' http://YOUR_ADDRESS:PORT/api/notes
The -i flag prints the status line, so you can confirm HTTP/1.1 201 Created with your own eyes.
🎯 Good to know: This example keeps notes in a plain array, so they reset on every restart. That's fine for learning the shape — when you want them to persist, move the array to SQLite or a managed database. Add a database is the next step, and The classic todo REST API is this same design wired to real storage.
A few habits worth keeping
- One response per request. Every path through a handler must end in exactly one
res.json/res.send/res.status().end().returnthe early ones so code doesn't fall through and try to respond twice (ERR_HTTP_HEADERS_SENT). - Return errors as JSON too. A client parsing your success as JSON will choke on an HTML error page. Send
{ error: '...' }with the right code. - Version later, not never. When an API has real users, prefixing routes with
/api/v1/lets you change things without breaking them. - CORS is separate. If a website on another domain calls this API from the browser, you'll hit a CORS wall until you add the
corsmiddleware — see CORS errors explained.
Everything past this point — pagination, filtering, nested resources, OpenAPI docs — is standard Express and standard REST. The official Express guide at expressjs.com covers routing and middleware in depth, and express-validator.github.io documents every validation rule.
Next steps
- Sessions, JWT & passwords — put a login in front of it
- Rate limiting your API — stop abuse
- Add a database — make the data survive restarts
- Build an Express website + API — the starter this builds on