PLAINFINCH learn by building

Requests and responses

You will read the parts of a request and a response, and write a route that tells visitors what they sent.

What is inside a request?

A request is a short text message from a browser, or from any other program, to your server. It always has a method and a path, and usually some headers. Some requests also carry a body. Those four parts are everything the server ever learns about what the visitor wants.

The method is a verb. GET asks to read something, and POST sends something new, such as a filled-in order form. The path names the thing: /, /menu, /whoami. Headers are labelled notes about the request, one per line, such as which browser sent it, which languages the reader prefers, and which cookies it holds. The body is the payload, and a plain GET has none.

Here is roughly what your browser sends when someone opens the bakery menu. The first line holds the method, the path and the protocol version. Every line after it is a header, written as a name, a colon and a value.

GET /menu HTTP/1.1
Host: localhost:3000
User-Agent: PlainFinch practice browser
Accept: text/html,application/json;q=0.9,*/*;q=0.8
Accept-Language: en-GB,en;q=0.9

What comes back in a response?

A response has three parts. The status line opens with a number that says how the request went: 200 means it worked, 404 means nothing lives at that path. Response headers describe the answer, the most important being Content-Type, which tells the browser whether the body is HTML, plain text, JSON or an image. The body is the content itself.

The browser reads them in that order. It checks the status first, then the headers, and only then decides what to do with the body. An HTML body with Content-Type: text/html gets drawn as a page. The same text labelled text/plain gets shown as raw characters, tags and all.

A request and its response always come as a pair. One request in, one response out. A server can never send a second response to the same request, and it can never send a response nobody asked for.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 29

Rye, sourdough, cinnamon buns
FigureRequest GET /menu with Host, User-Agent and Accept headers goes to the bakery server; the response comes back as status 200 OK, X-Powered-By, Content-Type and Content-Length headers, then the body Rye, sourdough, cinnamon buns.Request GET /menu with Host, User-Agent and Accept headers goes to the bakery server; the response comes back as status 200 OK, X-Powered-By, Content-Type and Content-Length headers, then the body Rye, sourdough, cinnamon buns.
A request is a method, a path and headers; the response answers with a status, its own headers, then the body. res.send() filled in 200, Content-Type and Content-Length for you.

How do you read a request in Express?

Express turns the incoming message into the req object that your handler receives. You don’t parse any text yourself. req.method is the method in capitals, such as 'GET'. req.path is the path without any query string, so a request for /menu?sort=price has req.path equal to /menu.

Headers live in req.headers, an object whose keys are always lower case, because Node.js lowercases every header name as it arrives. The easier way to read one is req.get('user-agent'). It ignores case, so req.get('User-Agent') works too, and it returns undefined when the header is missing. Spelling still matters: user_agent with an underscore is a different name and gives undefined.

The body is the odd one out. Express leaves req.body as undefined until you add a body parser such as express.json(). That comes in later lessons, when the bakery starts taking orders.

app.get('/menu', (req, res) => {
  console.log(req.method);            // 'GET'
  console.log(req.path);              // '/menu'
  console.log(req.get('user-agent')); // 'PlainFinch practice browser'
  res.send('Rye, sourdough, cinnamon buns');
});

What does res.send() fill in for you?

When you call res.send('Rye, sourdough, cinnamon buns'), Express builds a whole response around that one string. It uses status 200 unless you chose another. It sets Content-Type: text/html; charset=utf-8 because you passed a string, and it counts the bytes for Content-Length. It also adds X-Powered-By: Express, a header the headers lesson shows you how to remove. A real Express server adds an ETag header to these answers as well, used for caching; the practice server leaves it out until the caching lesson.

The type follows what you pass. Give res.send() an object or an array and Express turns it into JSON and labels it application/json. If you have already set a type yourself, Express keeps yours.

Calling res.send() also finishes the response. After that the exchange is over, and anything else you try to send for the same request fails. That is why the Break It server below leaves an error in the Log.

How do you see both sides in the Requests view?

Press **GET /menu**. The Requests view draws the exchange as a conversation. On the way out you see the method and path. In the middle it names the route that handled it, here app.get('/menu'), with the pretend time it took. On the way back you see the status, the content type and the size of the body.

Open “Headers and body” to see everything: every request header the practice browser sent, every response header Express added, and the full body. When an answer surprises you, this is where you compare what was asked with what was sent.

Then switch to the Log. The line GET /menu was printed by your own console.log, tagged with the request that caused it. Reading the Requests view and the Log side by side is how you debug every server in this course.

Why would a server answer differently to different visitors?

Every request carries its own headers, so one route can give different answers to different senders. A phone and a laptop send different User-Agent strings. A reader in Paris might send Accept-Language: fr-FR. A logged-in member sends a cookie that a stranger doesn’t have. The route code is the same each time; only the request changes.

The /whoami task shows this in miniature. Press **GET /whoami** and then **Ask as another program**. The second request claims to be a program called FinchTest/1.0, and your answer changes to match, because the handler reads the header afresh on every request.

Treat every one of these values as a claim. Any program can send any User-Agent it likes, so never use it to decide who may see something. It is fine for statistics or for a friendly message, and useless as proof of anything.

What happens when a handler forgets to answer?

Express runs your handler and then waits for it to call res.send(), res.json() or another method that ends the response. If the handler logs something and returns without answering, the browser keeps waiting. Real browsers show a spinning tab and give up after a while. The practice server marks the exchange **No reply**.

The opposite mistake is answering twice. Two res.send() calls in one handler means the first one wins and the second throws an error named ERR_HTTP_HEADERS_SENT, which appears in the Log. The visitor only ever sees the first answer.

Both mistakes are easy to spot once you know the rule: every path through a handler must answer exactly once. When you read a route, follow each if branch to the end and check that it reaches a single res call.

  • No res call: the request hangs and shows **No reply**.
  • One res call: the normal case.
  • Two res calls: the visitor gets the first, and the Log shows ERR_HTTP_HEADERS_SENT.

What should you check when an AI writes a route?

AI tools write routes quickly and usually correctly, but a few slips repeat. Look for handlers that build a value and log it without sending it, for header names written with underscores or camel case, and for code that reads req.body without a body parser in place. Each one runs without a startup error and then misbehaves on the first real request.

Read every route with the request in mind. Ask which method and path reach it, which parts of req it reads, and which single line answers. Then send the request and compare the Requests view with what you expected. A five-second test catches most of these problems before a visitor does.

Field notes

  • A request carries a method, a path, headers and sometimes a body.
  • A response carries a status number, headers and a body.
  • Express hands you the request as req, so req.method and req.get() read it for you.
  • A handler must end with res.send() or a similar call, or the visitor keeps waiting.

Easy to get wrong

console.log() writes to the server’s Log, which only you can read. The browser gets nothing until the handler calls a method on res, so this request hangs with **No reply**.

Node.js stores header names in lower case, so the capitalised key in req.headers is always undefined. req.get() ignores case and is the simpler way to read a header.

A response is sent once. Build the whole body first and send it in one call; the second call throws ERR_HTTP_HEADERS_SENT and its content is lost.

Questions people ask

Is the request really plain text?

In HTTP/1.1, yes: the method line and headers travel as readable text, which is why the examples look the way they do. HTTP/2 and HTTP/3 pack the same parts into a compact binary form, but Express gives you the same req object either way.

Where does the User-Agent in the practice server come from?

The request bar and the Browser view send PlainFinch practice browser. A real browser sends a long string naming the browser and operating system. You can send any value you like by adding a header to a request, as the **Ask as another program** button does.

What is the difference between req.path and req.url?

req.path is only the path, such as /menu. req.url keeps the query string too, such as /menu?sort=price. Use req.path when you care which page was asked for and req.query when you need the extra values.

Can a response have no body?

Yes. Some statuses, such as 204 No Content and 304 Not Modified, never carry a body, and a HEAD request asks for the headers only. The status and headers still arrive, which is often all the client needs.

Keep going