Express.js Request & Response

12 min readFeb 28, 2025

A detailed look into Express.js Request & Response objects.

This blog is a continuation of my Express.js series. Today’s topic is reading the various information from the request and different ways of handling the response. If you’re new to the series, this is the place to start:

3 stories

The request object is responsible for reading incoming information from the client calling the API.

Client Information

Here is how you can read details about the client who is using your API:

app.get('/client-info', (req, res) => {

res.send({
isXHR: req.xhr,
origin: req.headers['user-agent'],
language: req.headers['accept-language'],
clientIP: req.headers['x-forwarded-for'] ?? req.socket.remoteAddress,
});

});

The response will vary depending on where you call this route.

CURL (terminal) response

If I make the request using CURL:

> curl http://localhost:3000/client-info 

The output will be as follows:

{
"isXHR": false,
"origin": "curl/8.9.1",
"clientIP": "::1"
}

The response indicates:

  • isXHR = false — The request was not made from the browser as I made this request using CURL
  • origin = "curl/8.9.1" — The request was made using CURL
  • clientIP = "::1" — The client’s IP address is localhost (as I’m running this server locally)

Browser response

Running the same request in the XMLHttpRequest header and Fetch API:

const URI = 'http://localhost:3000/user-info'

fetch(URI,
{
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest' // <-- add this header
}
}
)
.then(res => res.text())
.then(data => console.log(data))
.catch(err => console.error(err));

Returns the following:

{
"isXHR": true,
"origin": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
"language": "en-US,en;q=0.9",
"clientIP": "::1"
}

The response indicates:

  • isXHR = true — Confirms that the request was made from the browser. This helps the server distinguish between AJAX (XHR) and other requests, which is good for logging and preventing CSRF attacks.
  • origin = "Mozilla/5.0 (Windows NT 10.0;)..." — The information on the underlying OS (Windows 10 x64) and browser versions.
  • language = "en-US" — The preferred browser language locale. If your application uses multiple languages, you can use the client’s locale to set the app language to their preferred language.
  • clientIP — retrieves the client’s IP, which can be used to determine the client’s location (using GEO IP)

Likewise, you can read the HTTP information of the request:

app.get('/http-info', (req, res) => {

res.send({
protocol: req.protocol,
httpVersion: req.httpVersion,
httpMethod: req.method,
usesSSL: req.secure,
});

});
> curl http://localhost:3000/http-info

Which produces the response:

{
"protocol": "http",
"httpVersion": "1.1",
"httpMethod": "GET",
"usesSSL": false
}

The response indicates:

  • The protocol used: HTTP (default REST API protocol)
  • HTTP Version: 1.1
  • HTTP Method used: GET
  • Is a secured (HTTPS) connection used — No, usesSSL = false.

This information can be used to log incoming requests. You can separate requests per HTTP Methods or Protocols.
Another purpose is redirecting. For example, if the particular feature has HTTP and HTTPS routes, you can redirect the users using the old API endpoint to the newer (HTTPS).

Server Information

The request object also contains information on the running server:

  • Server IP: req.ip (::1)
  • Hostname: req.hostname (localhost)
  • Port: req.socket.localPort (3000)
  • Path: req.originalUrl (the path you’re visiting)

So if you combine these, you’ll get:

localhost:3000/YOUR-PATH

As well as information on timeouts:

  • Request timeout: req.socket.server.requestTimeout (300000 ms)
    The maximum time the server will wait for a request to be completed before timing it out.
  • Headers timeout: req.socket.server.headersTimeout(60000 ms)
    The maximum time the server will wait for the complete headers of an HTTP request to be received.
  • Keep-alive timeout: req.socket.server.keepAliveTimeout(5000 ms)
    Defines how long the server should keep an idle connection open when using HTTP persistent connections (keep-alive) after completing a response.

Working with Data

The object comes packed with methods containing information about the data in the request, such as:

  • Parameters
  • Query
  • Headers
  • Cookies (requires cookie-parser middleware)
  • Body (requires body-parser middleware for versions < 4.13)

Request Parameters

The dynamic input parameters are read via req.params object, (e.g. req.params.id) where id is the name of the parameter in the route:

app.get('/:id', (req, res) => {
const id = req.params.id;
res.send(id);
});

> curl http://localhost:3000/123
=> 123

> curl http://localhost:3000/faruk
=> faruk

The dynamic parameter can be optional, and it can have pattern-based constraints as well—more on that in my Express.js Router blog.

Request query

Just like with route parameters, you can use the req.query to read data from the query object:

> curl http://localhost:3000?name='Mirza'

app.get('/', (req, res) => {
const query = req.query;
res.send(query);// {"name":"'Mirza'"}
});

Request headers

Use the req.headers property to read all headers from an incoming request. A more subtle way is to read the specific headers by name:

app.get('/', (req, res) => {

// read the specific header by name
const accessToken = req.header('access-token');
if (accessToken) {
res.send('Access token passed!');
} else {
res.status(401).send('Error! Missing access token!');
}
});

Further, you can add different logic when a user does or doesn’t send the header:

> curl http://localhost:3000 // Error Path
Error! Missing access token!

> curl http://localhost:3000 -H "access-token: XYZ" // Happy Path
Access token passed!

Request Cookies

Likewise, you can read the data from cookies. This feature is not native to Express.js and requires installing the third-party package cookie-parser.

> npm i cookie-parser

Afterward, import the cookie-parser to your app.

const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

app.use(cookieParser()); // inject middleware as per docs

app.get('/', (req, res) => {
const cookies = req.cookies; // read all cookies (JS object)
res.send(cookies.CUSTOM_COOKIE); // optionally, send the cookie back
});
> curl http://localhost:3000 --cookie "CUSTOM_COOKIE=Pezo"

Request body

Lastly, you can read the incoming request body using the body-parser middleware. As of Express.js version 4.13, this feature is now native to Express. Here is how to implement it:

const express = require('express');
const app = express();

app.use(express.json()); // inject body-parser middleware

app.post('/', (req, res) => {
const data = req.body; // read request body
res.send(data.username);
});

The request body does not work on GET requests. Hence, this example uses POST. Here is a simple request:

// for Unix
> curl -X POST -H "Content-Type: application/json" --data '{"username":"Mirzly", "profession":"DEV"}' http://localhost:3000

// for Windows
> curl -X POST -H "Content-Type: application/json" --data "{\"username\":\"Mirzly\", \"profession\":\"DEV\"}" http://localhost:3000

The Response object (res) specifies the HTTP response, which an Express app sends when it gets an HTTP request. It is responsible for sending the appropriate response to the client after processing the request. The response can vary:

  • Send different content types (plain text, HTML, JSON, binary)
  • Use different status codes (200, 201, 204, 400, 404, 500, etc.)
  • Set custom headers and cookies
  • Create a redirect to a temporary route or a completely new domain
  • Download the file to the client
  • Render a template page

There are two ways of sending the response from the server to the client:

  • res.send()
  • res.write() + res.end()

Response.Write()

The res.write() method returns plain text and stacks all responses inline. It is used when you need to send data continuously, like sending data in chunks when using streams or server-sent events.

This approach requires manual control of handling the response. Thus res.write() is followed by res.end();

app.get('/', (req, res) => {
res.write('1');
res.write('2');
res.write('3');

res.end();
});

To return JSON objects with res.write(), you need to use the JSON.stringify() method to convert plain text to JSON:

res.write(`data: ${JSON.stringify(data)}\n\n`);
res.end();

If you wish to keep the connection open, you don’t set res.end() at all. In this case, you must also instruct the client (e.g., the browser) to keep the connection open by sending response headers.

Here is how to keep the connection alive using the
‘Connection’, ‘keep-alive’ and ‘Content-Type’, ‘text/event-stream’ headers.

app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write('hello!');
});

Server-Sent Events with Express.js

This can be extended to the Server-Sent events example, where the browser is instructed to keep the connection open for all upcoming responses sent from the server.

app.get('/events', (req, res) => {

// these headers tell browser what type is receiving
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};

sendEvent({ message: 'Hello!' });
const interval = setInterval(() => sendEvent({ timestamp: Date.now() }), 1000);

// clean up when the client disconnects
req.on('close', () => clearInterval(interval));
});

Response.Send()

The res.send() method is used for one-time responses from the server to the client. The example of this a standard RESTful request (GET, POST, UPDATE, DELETE) or rendering HTML to the browser.

app.get('/', (req, res) => {
res.send('Hello World!');
});

Since it’s a one-time response, res.send() is not followed by res.end().

Content-Types

The res.send() method can recognize a set of default mime types and set appropriate response headers automatically:

  • Plain text
app.get('/', (req, res) => {
res.send('Hello World!'); // text
});
  • HTML content
app.get('/', (req, res) => {
res.send('<h1>Hello World!</h1>'); // HTML
});
  • JSON Content
app.get('/', (req, res) => {
res.send({ data: 'Hello World!' }); // JSON
});
  • Octet-stream binary data:
app.get('/', (req, res) => {
res.send(Buffer.from('Hello World!')); // binary
});

The HTML tags will appear as actual HTML content, JSON will be prettified, and binary data will be consumed. Using res.send(), Express.js takes care of appropriate response headers under the hood.

Other Content Types

For other content types, you’ll need to use set headers manually, e.g.:

res.writeHead(200, {
'Content-Type': 'video/mp4',
'Transfer-Encoding': 'chunked',
});

Overriding the default Content-Type

Should you need to override the default response headers (set by res.send()), the res.setHeader() method lets you do exactly that.

At first glance, these two endpoints return the HTML content.

app.get('/html', (req, res) => {
res.send('<h1>hello</h1>');
});

app.get('/html-as-json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send('<h1>hello</h1>');
});

However, I decided to override the default HTML content with the JSON in the second example. The result is that the first example shows correct HTML, while the second displays text as plain text.

You can also confirm by reading the response headers of each endpoint.

> curl http://localhost:3000/html -I

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
> curl http://localhost:3000/html-as-json -I

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

Content-Type shorthand

The Express.js also features several shorthand for setting headers via the Response object. In the case of the Content-Type header, you can use the res.type() shorthand.

  • Original
app.get('/html-as-json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send('<h1>hello</h1>');
});
  • Shorthand
app.get('/html-as-json', (req, res) => {
res.type('json'); // <-- shorthand
res.send('<h1>hello</h1>');
});

The output will be the same.
Feel free to look up other shorthand in the official documentation.

Response Headers

As seen before, using res.send() method, the appropriate content-type headers are set up automatically. However, there are times when you need to send additional headers. Here is an example of that:

app.get('/', (req, res) => {
res.setHeader('Authorization', 'XYZ');
res.setHeader('Fav-Dish', 'Carpaccio');
res.status(200).send({ greetings: 'Hello!' });
});
Look at browser dev tools, Network tab

When you call this endpoint, two custom response headers appear in the browser dev tools Network tab, as expected.

The response headers can pass important information to the client (like authorization token) or instruct the browser how to handle the response (like starting the stream).

Set Header vs Write Head

There are two main methods to set the response headers:

  • res.setHeader()
  • res.writeHead()

The res.setHeader() method is used when you want to set headers individually before writing the response body. It can be called multiple times.

res.setHeader('Content-Type', 'application/json');
res.setHeader('X-TOKEN', '12345');

res.send({ message: 'Hello, world!' });

Express internally uses res.setHeader() to modify headers dynamically. The method allows modifying headers multiple times:

res.setHeader('X-TOKEN', '12345');

// Later, you can modify a header before sending the response
res.setHeader('X-TOKEN', '007');

The res.writeHead() method is used when sending the headers immediately with the status code. It must be called before writing the response body and can be called exactly once.

res.writeHead(200, {
'Content-Type': 'application/json',
'X-TOKEN': '12345'
});

res.send({ message: 'Hello, world!' });

Status Codes

The response default status code is 200 (OK). In case of an error, the status will be 500 (Internal Server Exception).

The response object can be used to send any HTTP Status Code you desire:

  • 204 (No Content)
app.get('/No-Content', (req, res) => {
res.status(204).send('No Content!');
});
  • 400 (Bad Request)
app.get('/Bad-Request', (req, res) => {
res.status(400).send('Bad Request!');
});
  • 404 (Not Found)
app.get('/Not-Found', (req, res) => {
res.status(404).send('Not Found!');
});
  • 502 (Bad Gateway)
app.get('/Bad-Gateway', (req, res) => {
res.status(502).send('Bad Gateway!');
});

and similar.

Redirects

Express.js allows for redirects between routes using 301 and 302 status codes.

Temporary redirect (302)

The temporary redirect (also the default one) indicates that the resource has been temporarily moved to a new URL. As this redirection is temporary, the client should continue to use the original route URL for future requests.

// the request comes here
app.get('/', (req, res) => {

// grab auth token from request headers
const token = req.headers.AuthToken;
if (!token) {
res.status(302).redirect('/sign-up');
} else {
res.send('Welcome user!');
}
});

// redirect to this route
app.get('/sign-up', (req, res) => {
// send response to the client
res.send('Enter username and password to continue!');
});

P.S. Usually, logic like this is done in the middleware.

Permanent redirect (301)

This type of redirect indicates that the resource has been permanently moved to a new URL. The client (browser, search engine, etc.) should update its records and always use the new URL in future requests.

It is useful when you want to permanently move the client from one domain URL to another.

// the request comes here
app.get('/', (req, res) => {
// redirects to this site
res.status(301).redirect('https://google.com');
});

Response Cookies

Likewise, you can send data in the cookie. Each cookie has a name and a value:

app.get('/', (req, res) => {
res.cookie('message', 'Take my cookie!');
res.status(200).send({ greetings: 'Hello!' });
});

The cookie will appear in the browser dev tools application tab in the cookies section.

Website cookies visible in browser dev tools

Optionally, you can pass a set of options as a third parameter.

HTTP-Only Cookie

Since cookies are prone to various malicious attacks, such as cross-site scripting or man-in-the-middle attacks, it’s a good idea to keep them secured. One approach is to use the HTTP-Only cookies. Here is how it works:

app.get('/', (req, res) => {

res.cookie('secureCookie', {
secure: true, // only works with https
httpOnly: true,
maxAge: 300000 // for how long (in seconds) will this cookie be valid
});

res.status(200).send({ greetings: 'Hello!' });
});

The secure cookie also appears in the dev tools (application tab), but the value is encrypted.

File Download

The res.download() function in Express.js allows you to download files from the server.
I created three files in the root / public directory.

The res.download() file requires a path to the file. Once the route is hit, the file will instantly download in the browser.

  • Download an HTML file
app.get('/HTML', (req, res) => {
res.download(__dirname + '/public/index.html');
});
  • Download a JSON file
app.get('/JSON', (req, res) => {
res.download(__dirname + '/public/data.json');
});
  • Download an image file
// Download an Image file
app.get('/Image', (req, res) => {
res.download(__dirname + '/public/lion.png');
});

Response Event Listeners

Since Express.js is derived from the Node.js HTTP module, the request and response objects carry over the events you can listen to. The Response object, in particular, (is an instance of http.ServerResponse) can be used to listen to changes in the request-response flow.

Run code after the response is sent

This can be done by listening to the finish event on the response object. This can be used as a confirmation log that the response was indeed sent to the client:

app.get('/hello', (req, res) => {

res.send('Hello World!');

res.on('finish', () => {
console.log('Greeting was sent to the client!');
});

});
> curl http://localhost:3000/hello
Hello World!

[LOG]: Greeting was sent to the client!

Run code on request termination

This can be used to keep track of the requests that have never finished.

app.get('/slow', (req, res) => {

setTimeout(() => {
res.send('Hello World!');
}, 10_000);

res.on('close', () => {
console.log('The client terminated the connection before the response was sent!');
});

});
> curl http://localhost:3000/hello
--> Cancel the request

[LOG]: The client terminated the connection before the response was sent!

Other options include:

  • res.on(‘error’) — listening to global errors
  • res.on(‘drain’) — handling backpressure in writable streams.

Wrapping up

The Request object is used to read the information from the client and send it for further processing, while the Response object is responsible for sending appropriate data back to the client.
Stick around for more content on Express.js.

Bye for now 👋

--

--

Mirza Leka
Mirza Leka

Written by Mirza Leka

Web Developer. DevOps Enthusiast. I share my experience with the rest of the world. Follow me on https://twitter.com/mirzaleka for news & updates #FreePalestine

No responses yet