Process HTTP Requests in Express

19 min read·Jan 1, 2025

In Express, incoming HTTP requests can be intercepted before they reach a route's request listener using a special type of function called a middleware.

These middlewares are at the core of Express' design and are used for a variety of actions, such as logging requests, verifying request headers, parsing request payloads, and so on.

Define middleware functions

Just like a request listener, a middleware has access to the request and response objects, as well as an additional parameter named next.

function middleware(req, res, next) {
  //
}

Modify the request-response lifecycle

Beyond executing internal logic, a middleware can terminate the request-response lifecycle by sending an immediate response to the client using one of the methods of the response object, such as send() or sendStatus():

function middleware(req, res, next) {
  // Execute internal logic
  res.sendStatus(200);
}

Or it can forward the request to the next middleware (or request listener) in the call stack, by invoking its next() argument, which is actually a function — as without it, the request will be left hanging until it times out:

function middleware(req, res, next) {
  // Execute internal logic
  next();
}

Note: Any modifications made to the request and response objects will be preserved and passed on to each component of the middleware stack, which is useful if you want to forward custom properties such as IDs or connection tokens.

Use middleware functions

In Express, middleware functions can be used at three different levels:

  • The application-level.
  • The route-level.
  • The router-level.

Application-level middlewares

An application-level middleware is a function used to intercept all requests or groups of requests as soon as they reach the server and before they are routed.

The most common use cases for this type of middleware include request logging, headers checking, and error handling.

These middlewares are bound to the application object through its use() method:

server.use([PATH,] MIDDLEWARE [, MIDDLEWARE]);

Where:

  • server is an Express instance created using the top-level express() function.
  • PATH is an optional mount path, a path pattern, a regular expression, or an array of combinations of these three.
  • MIDDLEWARE is a single middleware function or a series of middleware functions.

Example

Let's consider this server, whose application-level middleware will log every incoming HTTP request into a file named server.log:

const express = require('express');
const { appendFileSync } = require('node:fs');

const server = express();

server.use((req, res, next) => {
  try {
    const log = `${new Date().getTime()} ${req.ip} ${req.method} ${req.path}\n`;
    appendFileSync('server.log', log);
  } catch(error) {
    console.error(error.message);
  } finally {
    next();
  }
});

server.all('*', (req, res) => {
  res.sendStatus(200);
});

server.listen(3000);

When executed, it will:

  1. Catch every single incoming HTTP request reaching the server.
  2. Log the request's timestamp (new Date().getTime()), the client's IP address (req.ip), the request's verb (req.method), and the request's relative URL (req.path) into a file named server.log.
  3. Forward the request to the next route using the next() handler.
  4. Catch every single incoming HTTP request using the app.all() method combined with the * path.
  5. Respond to the client with a HTTP 200 OK using the res.sendStatus() method.

When sending the following HTTP requests to the server:

$ curl 127.0.0.1:3000/
OK
$ curl 127.0.0.1:3000/foo/bar
OK
$ curl -X POST 127.0.0.1:3000/bar
OK

The server will write these logs into the server.log file:

$ cat server.log
1732108282051 ::ffff:127.0.0.1 GET /
1732108297337 ::ffff:127.0.0.1 GET /foo/bar
1732108304042 ::ffff:127.0.0.1 POST /bar

Route-level middlewares

A route-level middleware is a function used to intercept all requests before they reach the request listener of a specified route.

These middlewares are declared in the route method, between the path string and the request listener:

server.METHOD(PATH, MIDDLEWARE[, MIDDLEWARE], HANDLER);

Where:

  • METHOD is a function representing an HTTP method.
  • PATH is a string of characters representing a URN.
  • MIDDLEWARE is a single middleware function or a series of middleware functions.
  • HANDLER is a request listener function.

Example

Let's consider this server, whose route-level middleware will check if the client accepts a response in the HTML format:

const express = require('express');

const server = express();

server.get('/',
  (req, res, next) => {
    const mediaTypes = /(text\/html|text\/\*|\*\/\*)/;
    
    if (!mediaTypes.test(req.headers?.accept)) {
      return res.sendStatus(406);
    }
    next();
  },
  (req, res) => {
    res
      .set('Content-Type', 'text/html')
      .send('<!DOCTYPE html><html><head><title>Home</title></head><body><h1>Welcome to Learn Backend</h1></body></html>');
  }
);

server.listen(3000);

When executed, it will:

  1. Check if the Accept HTTP request header is defined and includes the media types 'text/html', 'text/*', or '*/*' indicating that the client accepts a response in the HTML format, any text format, or any format at all.
  2. Break the request-response lifecycle by responding with an HTTP 406 Not Acceptable indicating that the server could not produce a response matching the list of acceptable formats, if it evaluates to false.
  3. Forward the request to the route's controller using the next() handler, if it evaluates to true.
  4. Respond with an HTTP 200 OK containing an HTML string, including the Content-Type header indicating that the response's body is in the HTML format.

When sending this HTTP request to the server, it should respond with an HTTP 200 OK:

$ curl -i -H 'Accept: */*' 127.0.0.1:3000
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 106
ETag: W/"6a-mXUkrHMj/FAM+Xf2eKe6Tg7Ubo0"
Date: Wed, 20 Nov 2024 12:00:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5

<!DOCTYPE html><html><head><title>Home</title></head><body><h1>Welcome to Learn Backend</h1></body></html>

Note: You can also try it in your browser at http://127.0.0.1:3000.

When sending this HTTP request to the server, it should respond with an HTTP 406 Not Acceptable:

$ curl -i -H 'Accept: application/json' 127.0.0.1:3000
HTTP/1.1 406 Not Acceptable
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 14
ETag: W/"e-dqk0oyZuM+x+D21Lq3ZqYJ94/o4"
Date: Wed, 20 Nov 2024 12:01:01 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Not Acceptable

Router-level middlewares

A router-level middleware is a function used to intercept all requests as soon as they reach the server and before they are forwarded to a specified router.

Just like application-level middlewares, these middlewares are bound to the application object through its use() method, between the optional path string and the router:

server.use([PATH,] MIDDLEWARE [, MIDDLEWARE], ROUTER);

Where:

  • server is an Express instance created using the top-level express() function.
  • PATH is an optional mount path, a path pattern, a regular expression, or an array of combinations of these three.
  • MIDDLEWARE is a single middleware function or a series of middleware functions.
  • ROUTER is a router instance created using the express.Router() function.

Example

Let's consider this server, whose router-level middleware will check if the client has exceeded its access limit:

const express = require('express');

const server = express();
const router = express.Router();

let clients = {};

router.get('/forecast', (req, res) => res.send('sunny'));

router.get('/airquality', (req, res) => res.send('good'));

server.use('/weather', (req, res, next) => {
  if (!clients[req.ip]) {
    clients[req.ip] = 1;
  } else {
    clients[req.ip] += 1;
    if (clients[req.ip] > 2) {
      return res.sendStatus(429);
    }
  }
  next();
}, router);

server.listen(3000);

When executed, it will:

  1. Catch every single incoming HTTP request reaching the server on the /weather path.
  2. Check if the client has already accessed the router's GET /weather/forecast and GET /weather/airquality endpoints more than 2 times based on its IP address.
  3. Break the request-response lifecycle by responding with an HTTP 429 Too Many Requests indicating that the client has sent too many requests.
  4. Forward the request to the appropriate router's endpoint using the next() handler.
  5. Respond in both cases with an HTTP 200 OK containing a string.

When sending these HTTP requests to the server, it should respond with an HTTP 200 OK:

$ curl 127.0.0.1:3000/weather/forecast
sunny
$ curl 127.0.0.1:3000/weather/airquality
good

When sending this HTTP request to the server, it should respond with an HTTP 429 Too Many Requests:

$ curl 127.0.0.1:3000/weather/forecast
Too Many Requests

Summary

Here's a summary of what you've learned in this lesson:

  • Middleware functions are used to intercept incoming HTTP requests.
  • Middleware functions have access to the request object, the response object, and a next() handler.
  • Middleware functions can break the request-response lifecycle by sending an immediate response to the client using methods such as send(), sendStatus(), etc.
  • Middleware functions can forward requests to the next component in the call stack using the next() handler.
  • Application-level middlewares are attached to the application using the following syntax: app.use([PATH,] MIDDLEWARE [, MIDDLEWARE]).
  • Route-level middlewares are attached to routes using the following syntax: app.METHOD(PATH, MIDDLEWARE[, MIDDLEWARE], HANDLER).
  • Router-level middlewares are attached to routers using the following syntax: app.use([PATH,] MIDDLEWARE [, MIDDLEWARE], ROUTER).

Enjoying the courses?

I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:

Support BackendBrewery
Process HTTP Requests in Express.js | Backend Brewery