🔀 Understanding Express' middleware functions

Tue Sep 28 2021

Express is a web framework for NodeJS that enables the development of web servers to be used for a variety of applications. From a simple API endpoint to a web server with a rendering engine. The way the business logic is executed in the Express application is with middleware functions.

What are middleware functions in Express?

They are normal JavaScript functions that accept 3 parameters: request, response and next. These functions will be the ones dealing with the request that reaches your Express server.

They can execute any kind of code: reading users from a database, calling a 3rd party API, storing files in a S3 bucket, etc.

Here is an example of a middleware function:

const app = express();

// This will be executed for all requests
app.use((req, res, next) => {
  console.log('New request');
  next();
});

app.listen(8080);

How are they executed?

The Express application is the one that executes them in the same order they have been registered as long as the path specified (if any) matches the request's url path. The HTTP method (GET, POST, PUT...) can also be specified when registering these functions and the requests will be routed accordingly.

Here is another example:

// Will be executed for each request
app.use((req, res, next) => {
  console.log('New request incoming');
  next();
});

// Will be executed only for GET requests to /users
app.get('/users', (req, res, next) => {
  console.log('New GET request to /users');
  next();
});

The request object

It is a normal JavaScript object containing information about the HTTP request: the URL, the body, the cookies sent by the client, etc.

Among other properties, it contains the following ones:

  • req.body: body of the request
  • req.cookies: contains cookies sent by the client
  • req.path: the URL path
  • req.query: an object containing a property for each query-parameter in the route

It also contains these methods:

  • req.accepts('content-type'): checks if the content type is acceptable
  • req.get('header-name'): returns the specified request header field

You can add more fields to the request object so the next middleware functions can read it.

The response object

It contains methods to send data back to the client. It can be used to modify the status code, the headers and the body response:

  • res.status(200): sets the HTTP status code
  • res.sendStatus(404): sets the HTTP status code and sends the registered status message as the body
  • res.send({ name: 'John', age: 30 }): sends the passed object as the body

The send method can only be called once, otherwise you'll get an exception.

The next callback

It is a callback function that can be invoked to delegate the execution to the next middleware.

Middleware functions must invoke this callback unless the response is sent to the client, otherwise the request will be left hanging.

If there is more code after the next() callback it will be executed once the next middleware is finished:

app.use((req, res, next) => {
  console.log('Previous middleware has called next()');
  next(); // Next middleware will be executed
  console.log('Next middleware has just finished its execution');
});

A request lifecycle

Imagine that you have a server with the following middleware functions:

app.use((req, res, next) => {
  console.log('Beginning of middleware 1');
  next();
  console.log('End of middleware 1');
});

app.use((req, res, next) => {
  console.log('Beginning of middleware 2');
  next();
  console.log('End of middleware 2');
});

app.use((req, res, next) => {
  console.log('Beginning of middleware 3');
  res.sendStatus(200);
  console.log('End of middleware 3');
});

When a request reaches the server this is what you would see in the logs:

// Beginning of middleware 1
// Beginning of middleware 2
// Beginning of middleware 3
> Response is sent to the client
// End of middleware 1
// End of middleware 2
// End of middleware 3