Grokking ExpressJS
Vote

0% completed

​

Handling Routes and HTTP Methods

One Path, Several Methods

The Routes for the List

The Routes for One Task

Grouping Methods for One Path with app.route()

Methods That Express Answers for You

Routes Are Checked in Order

Path Syntax That Changed in Express 5

Answering Requests That Match No Route

Moving Routes into Their Own File

What decides which handler runs when a request for DELETE /tasks/2 arrives? Two values from the request line do: the method and the path. A route registered with app.delete('/tasks/:id', ...) matches that request, while a route registered with app.get('/tasks/:id', ...) does not, even though its path is the same. Deciding which handler receives a request is called routing, and this lesson builds the complete set of routes that the task API needs.

One Path, Several Methods

An HTTP API usually treats a path as the name of a thing, called a resource, and uses the method to say what to do with it. The path /tasks names the list of tasks, and /tasks/2 names one task. The five methods that an API uses most often each have a fixed meaning:

MethodPathMeaningStatus on success
GET/tasksReturn the list200 OK
POST/tasksCreate a new task in the list201 Created
GET/tasks/:idReturn one task200 OK
PUT/tasks/:idReplace the whole task200 OK
PATCH/tasks/:idChange some fields of the task200 OK
DELETE/tasks/:idRemove the task204 No Content

Express has one method on app for each HTTP method, named after it in lowercase: app.get(), app.post(), app.put(), app.patch(), and app.delete(). Each one takes a path and one or more functions, and the last function is the handler.

The Routes for the List

The two routes for /tasks read the list and add to it:

const express = require('express'); const app = express(); app.use(express.json()); const tasks = [ { id: 1, title: 'Write the README', done: true }, { id: 2, title: 'Buy milk', done: false }, ]; let nextId = 3; app.get('/tasks', (req, res) => { const search = req.query.search; if (!search) { return res.json(tasks); } const term = search.toLowerCase(); res.json(tasks.filter((task) => task.title.toLowerCase().includes(term))); }); app.post('/tasks', (req, res) => { const task = { id: nextId++, title: req.body.title, done: false }; tasks.push(task); res.status(201).location(`/tasks/${task.id}`).json(task); });

The GET route reads an optional query parameter, so GET /tasks?search=milk returns only [{"id":2,"title":"Buy milk","done":false}]. Query parameters are the usual way to search, filter, and paginate a list, because they modify a request for the same resource instead of naming a different one.

The POST route answers with 201 Created, which tells the client that a new resource exists. The method res.location() sets the Location header to the address of that resource, so the client knows where to find it:

HTTP/1.1 201 Created Location: /tasks/3 Content-Type: application/json; charset=utf-8 {"id":3,"title":"Call the plumber","done":false}

The route takes new IDs from the counter nextId, not from tasks.length + 1 as earlier lessons did. Once a task can be deleted, the length of the list no longer tells you which IDs are free. After deleting task 1 from a list of two, tasks.length + 1 is 2 again, and the API would create a second task with the ID 2.

The Routes for One Task

The four routes for /tasks/:id all start by finding the task, so a small function does that once:

function findIndex(req) { return tasks.findIndex((task) => task.id === Number(req.params.id)); } app.get('/tasks/:id', (req, res) => { const index = findIndex(req); if (index === -1) { return res.status(404).json({ error: 'Task not found' }); } res.json(tasks[index]); }); app.put('/tasks/:id', (req, res) => { const index = findIndex(req); if (index === -1) { return res.status(404).json({ error: 'Task not found' }); } const { title, done } = req.body; tasks[index] = { id: tasks[index].id, title, done }; res.json(tasks[index]); }); app.patch('/tasks/:id', (req, res) => { const index = findIndex(req); if (index === -1) { return res.status(404).json({ error: 'Task not found' }); } const { title, done } = req.body; if (title !== undefined) tasks[index].title = title; if (done !== undefined) tasks[index].done = done; res.json(tasks[index]); }); app.delete('/tasks/:id', (req, res) => { const index = findIndex(req); if (index === -1) { return res.status(404).json({ error: 'Task not found' }); } tasks.splice(index, 1); res.sendStatus(204); });

The difference between PUT and PATCH shows when the client sends only part of a task. A PATCH /tasks/2 request with the body {"done":true} changes one field and keeps the title. A PUT /tasks/2 request with the same body replaces the whole task, so the title is gone:

PATCH /tasks/2 {"done":true} -> 200 {"id":2,"title":"Buy milk","done":true} PUT /tasks/2 {"done":true} -> 200 {"id":2,"done":true}

A PUT client must therefore send every field. In a real API, the PUT handler would reject a body with missing fields, and chapter 4 adds that validation. The DELETE route answers with 204 No Content and no body, because nothing is left to return.

Grouping Methods for One Path with app.route()

The four routes above repeat the path /tasks/:id. The method app.route() takes the path once and returns an object on which you chain one handler per method:

app.route('/tasks/:id') .get((req, res) => { /* same handler as above */ }) .put((req, res) => { /* same handler as above */ }) .patch((req, res) => { /* same handler as above */ }) .delete((req, res) => { /* same handler as above */ });

The result is the same set of routes. It only removes the repetition, and a misspelled path in one of four copies becomes impossible.

Methods That Express Answers for You

Two methods work without any route of your own. Express answers a HEAD request with the GET route for the same path, and sends the headers of that response without the body. Express also answers an OPTIONS request with an Allow header that lists the methods that the path supports. With the routes of this lesson registered, an OPTIONS /tasks/5 request receives Allow: DELETE, GET, HEAD, PATCH, PUT, and an OPTIONS /tasks request receives Allow: GET, HEAD, POST.

When the path exists but the method does not, as with DELETE /tasks, Express does not send 405 Method Not Allowed. It treats the request like any other request that no route matches, and it sends 404 Not Found with the HTML body Cannot DELETE /tasks.

Routes Are Checked in Order

Express compares a request with the routes in the order in which the code registered them, and the first route whose method and path both match handles the request. Suppose that you add a route for task statistics below the routes for one task:

app.get('/tasks/stats', (req, res) => { const done = tasks.filter((task) => task.done).length; res.json({ total: tasks.length, done }); });

A GET /tasks/stats request never reaches it. The route GET /tasks/:id comes first, and :id matches any segment, including the word stats. The handler converts 'stats' with Number(), gets NaN, finds no task, and sends {"error":"Task not found"} with the status 404.

GET /tasks/stats is compared with each route from the top, and the parameter route /tasks/:id matches it before the stats route is reached.
GET /tasks/stats is compared with each route from the top, and the parameter route /tasks/:id matches it before the stats route is reached.

The diagram shows the comparison stopping at the third route. To fix the order, register the route with the fixed path, /tasks/stats, above the route with the parameter, /tasks/:id. The request then returns {"total":2,"done":1}. As a rule, register specific paths before general ones.

Two more matching rules are on by default. Paths are compared without regard to case, so /TASKS matches /tasks, and a trailing slash is ignored, so /tasks/ matches /tasks as well.

Path Syntax That Changed in Express 5

Express 5 uses a stricter syntax for route paths than Express 4. Three patterns that are common in older code now throw an error when the route is registered, which stops the app at startup:

  • A bare wildcard. app.get('*', ...) throws Missing parameter name at index 1: *. A wildcard now needs a name, as in '/*splat'. It matches one or more segments, and req.params.splat holds them as an array, so /docs/guide/intro matched by '/docs/*path' gives req.params.path the value ['guide', 'intro']. To match the root path / as well, write '/{*splat}'.
  • An optional parameter with ?. '/tasks/:id?' throws Unexpected ? at index 10. An optional part is now written in braces, as in '/reports{/:year}', which matches both /reports and /reports/7.
  • Regular expression characters in a string. '/ab+cd' throws Unexpected + at index 3. If a route really needs a pattern, pass a regular expression literal, such as /^\/ab+cd$/, instead of a string.

Answering Requests That Match No Route

The default 404 response is an HTML page, which suits a browser but not an API client that expects JSON. To replace it, register a middleware function with no path after all routes. Express reaches it only when no route has already sent a response:

app.use((req, res) => { res.status(404).json({ error: `No route for ${req.method} ${req.path}` }); });

A DELETE /tasks request now receives {"error":"No route for DELETE /tasks"} with the status 404. If the app also has an error handler, place this function above the error handler, so that the error handler stays the last function in the stack. Older code often writes this handler as app.get('*', ...), which Express 5 rejects, as the previous section showed. The app.use() form also covers every method, not only GET.

The catch-all has one side effect. Express sends its automatic answer to an OPTIONS request only when no function has handled the request, and the catch-all handles every request that reaches it. With the catch-all registered, OPTIONS /tasks/5 received the 404 body {"error":"No route for OPTIONS /tasks/5"} instead of the Allow header. Most API clients never send OPTIONS themselves. Browsers do send one before some cross-origin requests, and the cors package, which chapter 6 introduces, answers those requests when you register it above the routes.

Moving Routes into Their Own File

As an API grows, one file with every route becomes hard to read. A router, introduced in the first lesson of this chapter, can hold all the routes for tasks in their own file, routes/tasks.js:

const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.json([]); }); router.get('/:id', (req, res) => { res.json({ id: Number(req.params.id) }); }); module.exports = router;

The main file then mounts the router on a path:

const taskRoutes = require('./routes/tasks'); app.use('/tasks', taskRoutes);

The paths inside the router are relative to the mount path. The router's '/' route answers GET /tasks, and its '/:id' route answers GET /tasks/2. Chapter 3 moves the whole task API into routers and shows how to group and version them.

Reading Progress

0%


Vote for new content

On This Page

One Path, Several Methods

The Routes for the List

The Routes for One Task

Grouping Methods for One Path with app.route()

Methods That Express Answers for You

Routes Are Checked in Order

Path Syntax That Changed in Express 5

Answering Requests That Match No Route

Moving Routes into Their Own File