Chapter 13 of 18 · Node.js for Beginners — Complete Free Tutorial
Routes and Route Parameters
Every API is a map of URLs to behaviour. The Node.js for Beginners series now builds that map with Express: defining routes for each HTTP method, extracting ids from paths with parameters, reading filters from query strings, and validating bodies — while laying the first stones of the to-do REST API you will finish in chapter 18.
Route anatomy
A route pairs a method and a path with a handler. The handler receives the request, writes the response, and Express takes care of everything else.
app.get('/api/todos', (req, res) => {
res.json({ todos: [] });
});
app.post('/api/todos', (req, res) => {
res.status(201).json({ id: 1, title: 'Learn routes', done: false });
});
curl -s http://localhost:3000/api/todos
curl -s -X POST http://localhost:3000/api/todos \
-H 'Content-Type: application/json' -d '{"title":"Learn routes"}'
Expected output:
{"todos":[]}
{"id":1,"title":"Learn routes","done":false}
The verbs map to the methods from chapter 10: app.get, app.post, app.put, app.patch, app.delete. Express 5 also accepts app.route(path) to group several methods for one path without repeating it.
Route parameters
A colon marks a placeholder — a parameter — which Express extracts into req.params.
app.get('/api/todos/:id', (req, res) => {
const { id } = req.params; // always a string
if (id !== '1') return res.status(404).json({ error: 'todo not found' });
res.json({ id: 1, title: 'Learn routes', done: false });
});
app.get('/api/todos/:id/comments/:slug', (req, res) => {
res.json(req.params); // both parameters together
});
curl -s http://localhost:3000/api/todos/1
curl -s http://localhost:3000/api/todos/1/comments/first
Expected output:
{"id":1,"title":"Learn routes","done":false}
{"id":"1","slug":"first"}
- Parameters arrive as strings — convert with
Number()or validate with a regex (/^\d+$/) before using them as numbers. - Express decodes percent-escapes for you, so
/todos/a%20byields'a b'. - A trailing wildcard such as
app.get('/files/*', …)changed in Express 5 — useapp.use('/files', …)or a named splat to stay version-proof.
Query strings
Everything after ? is optional filtering — never part of the path identity.
app.get('/api/todos', (req, res) => {
const { done, limit = '10' } = req.query;
let results = todos;
if (done !== undefined) results = results.filter((t) => String(t.done) === done);
res.json({ count: results.length, limit: Number(limit), todos: results.slice(0, Number(limit)) });
});
curl -s 'http://localhost:3000/api/todos?done=false&limit=5'
Expected output: {"count":2,"limit":5,"todos":[…]} — an object with the filtered rows. Values arrive as strings (or arrays for repeated keys), so coerce and bound them: never trust limit to be a sane number from a public caller.
Reading and validating the body
With express.json() registered, POST and PATCH bodies land on req.body. Validation stays your job — and the cheapest place to fail is the start of the handler:
app.post('/api/todos', (req, res) => {
const { title, done = false } = req.body ?? {};
if (typeof title !== 'string' || title.trim().length === 0) {
return res.status(400).json({ error: 'title must be a non-empty string' });
}
if (typeof done !== 'boolean') {
return res.status(400).json({ error: 'done must be a boolean' });
}
const todo = { id: nextId++, title: title.trim(), done };
todos.push(todo);
res.status(201).location(`/api/todos/${todo.id}`).json(todo);
});
Expected output: a valid body answers 201 with a Location header pointing at the new resource; a missing title answers 400 with a machine-readable message. Return early on failure so the happy path stays unindented — the guard-clause habit from Conditionals and Control Flow transfers directly.
Route order decides the winner
| Registration order | What happens |
|---|---|
| Specific route before generic | /api/todos/mine matches its own handler first |
Generic :id first | mine is captured as an id — confusing 404s |
| Catch-all 404 last | Unhandled paths fall through to a proper 404 instead of Express's HTML default |
Express matches top to bottom and stops at the first handler that responds. Register static paths and special segments before parameterised ones, and always finish with a not-found handler.
Common mistakes
- Trusting
req.paramstypes — ids are strings; compare and cast deliberately. - Skipping body validation — whatever the client sends becomes your data; validate at the edge.
- Registering routes in the wrong order — parameterised routes shadowing literal ones.
- Forgetting
returnbefore an error response — execution continues and may send a second response (Express then throws headers-sent errors). - Leaving off the 404 fallback — Express's default HTML error page breaks JSON clients.
Practice checklist
- Create GET, POST and DELETE routes for
/api/todosand exercise each with curl. - Add
/api/todos/:idand confirm an unknown id answers 404 with JSON. - Implement
?done=truefiltering and test both values plus the absent case. - Validate a body two ways (wrong type, empty string) and verify 400 responses.
- Register a literal route after
:idand observe it never matching — then fix the order.
Routes answer; the next building block decides what happens before they answer. Next: Middleware Explained.
Frequently Asked Questions
Node.js is a runtime that executes JavaScript outside the browser. It combines Google's V8 engine with APIs for files, networks, processes and streams, plus a package ecosystem (npm). You use it to build servers, command-line tools, APIs and anything that needs JavaScript running on a machine rather than in a page.
JavaScript is the language; Node.js is one environment that runs it. In the browser the language comes with a DOM, events and window APIs — in Node those are replaced by the file system, network and process modules. Syntax, functions, arrays, promises and async/await are identical, so your JavaScript knowledge transfers directly.
Browser code is sandboxed: it cannot read your files or open arbitrary ports. Node code has full machine access, which is why it is used for servers and tools but also why you never run untrusted code in it. There is no document or window in Node — the equivalents are global, process and modules.
npm is JavaScript's package manager and public registry. package.json describes your project: name, entry point, dependencies with version ranges, and scripts such as npm run dev. Installing a package copies it into node_modules and records the exact intent in package-lock.json so every machine installs identical versions.
Express is a minimal web framework for Node: a few thousand lines that give you routing, middleware, request parsing and static files on top of the raw HTTP server. It is the most common starting point for REST APIs and the backbone of this tutorial's project chapters.
Both work. CommonJS (require/module.exports) is the classic Node format; ES modules (import/export) are the standard and activated in a package.json with "type": "module". Modern projects favour ESM for browser compatibility, but you will read both in existing code, so this course teaches each.
Middleware are functions that run between the incoming request and the final response, in order: body parsing, logging, authentication, rate limits, error handling. Each receives the request, response and next function and can respond itself or pass control along — Express's pipeline model in one sentence.
A JSON file is perfect for learning CRUD: no setup, readable data, easy to reset. The moment you need concurrent writes, querying or multiple users, move to a real database — MySQL/PostgreSQL for relational data or MongoDB for documents. The storage chapter isolates persistence behind one module so swapping it later is straightforward.
A REST API exposes resources over HTTP using verbs on URLs: GET /api/todos lists, POST creates, PUT or PATCH updates, DELETE removes, and everything answers with JSON plus correct status codes (200, 201, 400, 404, 500). It is stateless — each request carries its own authentication — which makes it easy to cache, scale and consume from any frontend.
Node apps are long-running processes, not static files: deploy them to a Node-friendly host (a VPS, Render, Railway or Fly.io), keep secrets in environment variables, terminate HTTPS at a reverse proxy or platform edge, and send logs somewhere durable. The final chapter covers the full checklist including health checks and graceful shutdown.