Grokking ExpressJS
Vote

0% completed

​

Understanding the Request-Response Cycle

The Server for This Lesson

One Request, Step by Step

Reading the Request

Query Parameters

Route Parameters

Headers

The Request Body

Writing the Response

Exactly One Response per Request

Every exchange between a client and an Express server is two pieces of text. Below is a request that asks the task API for the tasks that are not done yet, followed by the response that the server sends back:

GET /tasks?done=false HTTP/1.1 Host: localhost:3000 User-Agent: curl/8.19.0 Accept: */* HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 42 ETag: W/"2a-NTmjGQN7OSr9t1gH82IrFh542Kc" [{"id":2,"title":"Buy milk","done":false}]

The request starts with a line that holds the method (GET), the path (/tasks), and an optional query string (?done=false). Header lines follow, and a request that carries data, such as a POST, adds a body after an empty line. The response starts with a status line (200 OK), continues with its own headers, and ends with a body.

Everything that happens between the arrival of the request and the sending of the response is called the request-response cycle. This lesson follows one request through that cycle and shows where your code can read the request and write the response.

The Server for This Lesson

The task API from the previous lesson now holds two tasks, prints a line for every request, and can filter the list:

const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; const tasks = [ { id: 1, title: 'Write the README', done: true }, { id: 2, title: 'Buy milk', done: false }, ]; app.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next(); }); app.get('/tasks', (req, res) => { if (req.query.done === undefined) { return res.json(tasks); } const done = req.query.done === 'true'; res.json(tasks.filter((task) => task.done === done)); }); app.listen(PORT, (error) => { if (error) { throw error; } console.log(`Server is running on port ${PORT}`); });

One Request, Step by Step

When a client sends GET /tasks?done=false to this server, the cycle runs in this order:

  1. The client opens a connection to port 3000 and sends the request text.
  2. Node.js parses the text into a request object and creates an empty response object. It then calls the Express app with both objects. Express adds its own properties and methods to them, and from now on your code sees them as req and res.
  3. Express runs the functions that were registered on the app, in the order in which the code registered them. The first one is the function passed to app.use(). It prints GET /tasks?done=false and calls next(), which tells Express to continue with the next function.
  4. Express compares the request with each route. The method is GET and the path is /tasks, so the route app.get('/tasks', ...) matches and its handler runs. The query string is not part of the path, so it does not affect the match.
  5. The handler reads req.query.done, filters the list, and calls res.json(). That call sets the status code and the headers, writes the body, and ends the response. The cycle for this request is now complete.
  6. The client receives the response and uses it. A browser displays it, and a frontend application usually turns the JSON back into objects.

A function like the one passed to app.use(), which runs before the route and decides whether the request continues, is called a middleware function. Chapter 2 is about middleware, so for now it is enough to know that every middleware function must either call next() or send a response.

Reading the Request

The req object holds every part of the request text in a form that is ready to use. The diagram below maps each part of a request to the property where your code finds it. So that one request can carry every part at once, it uses a POST request that adds a note to task 2, sent to a route registered as app.post('/tasks/:id/notes', ...).

Each part of the HTTP request text appears as a property of the req object in the handler.
Each part of the HTTP request text appears as a property of the req object in the handler.

Look at where each value ends up. The method and the path each become one property, and the query string becomes an object. A segment of the path becomes an entry in req.params, and the body appears in req.body only after a body parser has run.

Query Parameters

The part of the URL after ? is the query string, and each name=value pair in it is a query parameter. Express parses it into the object req.query, which the /tasks handler above reads. For GET /tasks?done=false, req.query is { done: 'false' }.

Every value in req.query is a string. The handler therefore compares req.query.done with the string 'true', because the value is never the boolean true. When a name appears twice, as in ?tag=home&tag=urgent, its value becomes the array ['home', 'urgent'].

Express 5 parses the query string with a simple parser by default. As a result, a name with brackets, such as ?filter[done]=true, stays the key 'filter[done]' instead of becoming a nested object. Express 4 used a parser that created nested objects, so older code that reads req.query.filter.done does not work on Express 5 without the setting app.set('query parser', 'extended').

Route Parameters

A query string is good for optional choices such as a filter. To identify one specific resource, the identifier is usually part of the path, as in /tasks/2. Add this route below the first one:

app.get('/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); });

A segment of a route path that starts with a colon, such as :id, is a route parameter. It matches any value in that position, and Express stores the value in req.params under the parameter's name. For GET /tasks/2, req.params is { id: '2' }.

Like query values, route parameters are always strings, so the handler converts the value with Number() before it compares it with the numeric id of each task. Without the conversion, the strict comparison 2 === '2' is false, and every request would get a 404 response:

GET /tasks/2 -> 200 {"id":2,"title":"Buy milk","done":false} GET /tasks/99 -> 404 {"error":"Task not found"}

Headers

Request headers are available in req.headers, an object whose keys are the header names in lowercase. The method req.get() reads one header and ignores the case of the name, so req.get('Content-Type') and req.get('content-type') return the same value.

The Request Body

A POST request that creates a task sends the new task in its body. Express does not read the body unless you ask it to, and in Express 5 req.body is undefined until a body parser has run. The built-in parser express.json() reads a JSON body and stores the parsed object in req.body. Add it, together with a route that creates a task:

app.use(express.json()); app.post('/tasks', (req, res) => { const task = { id: tasks.length + 1, title: req.body.title, done: false }; tasks.push(task); res.status(201).json(task); });

Place these lines before the call to app.listen(). Then send a task as JSON:

curl -i -X POST http://localhost:3000/tasks \ -H "Content-Type: application/json" \ -d '{"title":"Call the plumber"}'
HTTP/1.1 201 Created Content-Type: application/json; charset=utf-8 {"id":3,"title":"Call the plumber","done":false}

The Content-Type: application/json header matters. The JSON parser reads only bodies that declare that type, so if a client sends the same text with Content-Type: text/plain, req.body stays undefined. The handler then fails with TypeError: Cannot read properties of undefined (reading 'title'), and the client receives a 500 response.

Writing the Response

The res object decides what goes back to the client. Three of its methods appear in almost every handler:

  • res.send(value) sends a string, a Buffer, or an object. A string is sent as HTML unless you set another type.
  • res.json(value) converts a value to JSON and sets Content-Type: application/json.
  • res.status(code) sets the status code and returns res, so that you can chain it, as in res.status(201).json(task).

The status code tells the client what happened before it reads the body. The task API uses three codes so far: 200 OK for a successful read, 201 Created for a new task, and 404 Not Found for an id that does not exist.

Exactly One Response per Request

Each request gets exactly one response. Once a handler has sent it, the headers are already on their way to the client, and they cannot be changed. A second call to res.send() or res.json() therefore throws an error when it tries to set the headers again:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

This is why the handlers above write return res.json(tasks) and return res.status(404).json(...). The return stops the function after the response is sent, so the code below it, which would send a second response, never runs.

The opposite mistake is a handler that sends nothing. Express does not end the response for you, so the client waits until its own timeout expires. In a test with a one-second timeout, the request to such a handler failed with Timeout of 1000ms exceeded. Every path through a handler must therefore end in a response or in a call to next().

Reading Progress

0%


Vote for new content

On This Page

The Server for This Lesson

One Request, Step by Step

Reading the Request

Query Parameters

Route Parameters

Headers

The Request Body

Writing the Response

Exactly One Response per Request