Routing means deciding which code answers which request. Each route is a combination of an HTTP method and a path. A good API uses the method to say what to do and the path to say which thing, for example GET /api/tasks/7 means read task 7 and DELETE /api/tasks/7 means remove it. In this lesson you will build read-only task routes with parameters and filters, and then learn how to organise routes in separate files.
Route parameters
A segment of the path that starts with a colon is a parameter. Express puts the matching part of the real URL into req.params. For the route /api/tasks/:id and the request /api/tasks/2, req.params.id is the text '2'. Parameters are always strings, so convert them with Number() when you need a number.
Query strings
The part of the URL after a question mark is the query string, such as ?done=true&sort=title. Express parses it into req.query. Use the path for *which thing*, and the query string for *how to filter, sort or page the list*. Query values are also strings, and a key can come as an array if it is repeated, so check the value before using it. Try the next example.
import express from 'express';
const app = express();
const tasks = [
{ id: 1, title: 'Learn Node', done: true },
{ id: 2, title: 'Learn Express', done: false },
{ id: 3, title: 'Build an API', done: false },
];
app.get('/api/tasks', (req, res) => {
const { done } = req.query; // /api/tasks?done=true
let list = tasks;
if (done === 'true') list = tasks.filter((t) => t.done);
if (done === 'false') list = tasks.filter((t) => !t.done);
res.json(list);
});
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.listen(3000, () => console.log('Server running'));
curl "http://localhost:3000/api/tasks?done=false"
curl http://localhost:3000/api/tasks/2
curl -i http://localhost:3000/api/tasks/99
[{"id":2,"title":"Learn Express","done":false},{"id":3,"title":"Build an API"...}]
{"id":2,"title":"Learn Express","done":false}
HTTP/1.1 404 Not Found
...
{"error":"Task not found"}
Notice the filter. We compare done with the text 'true' and 'false', because the query string only holds text. Any other value, such as ?done=maybe, simply returns the full list here. In a stricter API you could answer 400. Notice also that the one-task route answers 404 with a JSON message when the id is not found. A request to /api/tasks/abc also gets 404, because Number('abc') is NaN, which matches no id.
Choose good status codes
200for a successful read or update.201after creating something, ideally with aLocationheader that gives its address.204for a successful delete with nothing to return.400when the client's data is invalid,404when the thing does not exist.- Do not return
200with a body that says{ error: ... }. Clients, tests and monitoring tools rely on the status code.
Route order matters
Express tries routes in the order you wrote them and uses the first one that sends a response. A general route placed before a specific one can swallow requests. For example, if /api/tasks/:id comes before /api/tasks/stats, then a request to /api/tasks/stats is matched with id equal to stats. Put the specific routes first and the ones with parameters after them.
Express 5 path syntax
Express 5 uses a stricter path syntax than Express 4. A plain * wildcard is no longer allowed, and app.get('/old/*', ...) throws an error on start-up. A wildcard must have a name, as in /files/*splat, and the matched parts are then found in req.params.splat. An optional part is written with braces, as in /users{/:id}, which matches both /users and /users/7. If you copy older Express 4 examples from the internet and get a Missing parameter name error, this is the reason.
Split routes into files with Router
When an app grows past a few routes, one file becomes hard to read. Express has Router, a small mini-app that holds a group of routes. You write the routes with paths that are relative to the place where you mount the router.
// routes/tasks.js
import { Router } from 'express';
export function tasksRouter() {
const router = Router();
router.get('/', (req, res) => res.json([])); // GET /api/tasks
router.get('/:id', (req, res) => res.json({})); // GET /api/tasks/5
return router;
}
// app.js
import { tasksRouter } from './routes/tasks.js';
app.use('/api/tasks', tasksRouter());
In the router file, the route '/' means the root of wherever the router is mounted. app.use('/api/tasks', ...) mounts it under that prefix. Wrapping the router in a function, as we did here, lets you pass dependencies in, such as a data store, and it makes the code easy to test. The final project in this course uses exactly this layout, with one router for tasks and one for login.
Keep URLs predictable
- Use nouns in the plural for things:
/api/tasks,/api/users. - Do not put verbs in paths: prefer
POST /api/tasksover/api/createTask. - Use lowercase letters and hyphens.
- Keep a prefix such as
/apior/api/v1, so you can add a front end on the same domain, and you can change the API later without breaking old clients.
Your task
- Create
server.jswith the routes above and test the three curl commands. - Add
?sort=titlesupport: when it is present, return the tasks sorted by title. - Add a route
GET /api/tasks/statsthat returns how many tasks are done and how many are not. Put it before the:idroute and test it. Then move it after and see what happens. - Move the task routes into
routes/tasks.jsusingRouter.
How to check it worked
/api/tasks?done=truereturns only tasks that are done./api/tasks/99returns status 404 with a JSON message./api/tasks/statsreturns your counts, not a 404.