Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

How to extend the Express Request object in TypeScript

10 min read 2906

TypeScript Express Logo

The Request object is used by Express to provide data about the HTTP request to the controllers of a Node.js server. Therefore, all data from the request available at the application layer depends on this object.

You might need to elaborate on the data you received in the HTTP request and provide the controllers with custom info. In JavaScript, you can simply define new properties in the Request object and use them when needed. In TypeScript, you need to extend the Request type if you want to define custom properties.

Let’s now learn what Request is in Express, and delve into the reasons why extending the Request type in TypeScript can be useful. Then, let’s see how you can take advantage of the extended Request object through an Express demo application built in TypeScript.

TL;DR: Let’s learn how to extend the Request type in TypeScript to make its instances store custom data you can use at the controller level.

What is the Request object in Express?

The Request object represents the HTTP request performed by the client to an Express server. In other words, an Express server can read the data received from the client through Request instances. Therefore, Request has several properties to access all the information contained in the HTTP request, but the most important ones are:

query: this object contains a property for each query string parameter present in the URL of the request:
app.get("/users", (req: Request, res: Response) => {
  // on GET "/users?id=4" this would print "4"
  console.log(req.query.id)
});
params: this object contains the parameters defined in the API URL according to the Express routing convention:
app.get("/users/:id", (req: Request, res: Response) => {
  // on GET "/users/1" this would print "1"
  console.log(req.params.id)
});
body: this object contains key-value pairs of data submitted in the body of the HTTP request:
app.get("/users", (req: Request<never, never, { name: string; surname: string }, never>, res: Response) => {
  const { name, surname } = req.body

  // ...
})
headers: this object contains a property for each HTTP header sent by the request.
cookies: when using the cookie-parser Express middleware, this object contains a property for each cookie sent by the request

Why extend Request?

Express controllers can access all the data contained in an HTTP request with the Request object. This does not mean that the Request object is the only way to interact with the controllers. On the contrary, Express also supports middlewares. Express middlewares are functions that can be used to add application-level or router-level functionality.

The middleware functions are associated with the endpoints at the router level as follows:

const authenticationMiddleware = require("../middlewares/authenticationMiddleware")
const FooController = require("../controllers/foo")

app.get(
  "/helloWorld",
  FooController.helloWorld, // (req, res) => { res.send("Hello, World!") }
  // registering the authenticationMiddleware to the "/helloWorld" endpoint
  authenticationMiddleware,
)

Note that middleware functions are executed before the controller function containing the business logic of the API is called. Learn more about how they work and what the Express middlewares can offer here.

What is important to notice here is that middlewares can modify the Express Request object, adding custom information to make it available at the controller level. For example, let’s say you want to make your APIs available only to users with a valid authentication token. To achieve this, you can define a simple authentication middleware as follows:

import { Request, Response, NextFunction } from "express"

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
  const authenticationToken = req.headers["authorization"]

  if (authenticationToken !== undefined) {
    const isTokenValid = // verifying if authenticationToken is valid with a query or an API call...

    if (isTokenValid) {
      // moving to the next middleware
      return next()
    }
  }

  // if the authorization token is invalid or missing returning a 401 error
  res.status(401).send("Unauthorized")
}

When the authentication token received in the Authorization header of the HTTP request is valid, this value is uniquely associated with a user of your service. In other words, the authentication token allows you to identify the user who is making the request, which is very important to know. For example, the business logic at the controller level may change depending on the user’s role.

If several controller-level functions need to know who the user who performed the API call is, you have to replicate the logic required to retrieve the user from the Authorization header in multiple places. Instead, you should extend the Express Request object with a user property and give it a value in the authentication middleware.

Notice that the Express Request type in TypeScript does not involve a user property. This means that you cannot simply extend the Request object as follows:

import { Request, Response, NextFunction } from "express"

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
  const authenticationToken = req.headers["authorization"]

  if (authenticationToken !== undefined) {
    const isTokenValid = // verifying if authenticationToken is valid with a query or an API call...

    if (isTokenValid) {
      const user = // retrieving the user info based on authenticationToken  

      req["user"] = user // ERROR: Property 'user' does not exist on type 'Request'

      // moving to the next middleware
      return next()
    }
  }

  // if the authorization token is invalid or missing returning a 401 error
  res.status(401).send("Unauthorized")
}

This would lead to the following error:

Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.

Similarly, you can use an extended Request to avoid type casting at the controller level and make your codebase cleaner and robust. Let’s assume your backend application supports only three languages: English, Spanish, and Italian. In other words, you already know that the Content-Language HTTP headers can only accept en, es, and it. When the header is omitted or contains an invalid value, you want the English language to be used as default.

Keep in mind that req.headers["Content-Language"] returns a string | string[] | undefined type. This means that if you want to use Content-Language header value as a string, you have to cast it as follows:

const language = (req.headers["content-language"] || "en") as string | undefined

However, filling your code with this logic is not an elegant solution. Instead, you can use a middleware to extend Request as below:

import { Request, Response, NextFunction } from "express"

const SUPPORTED_LANGUAGES = ["en", "es", "it"]
// this syntax is equals to "en" | "es" | "it"
export type Language = typeof SUPPORTED_LANGUAGES[number] 

export function handleCustomLanguageHeader(req: Request, res: Response, next: NextFunction) {
  const languageHeader = req.headers["content-language"]

  // default language: "en"
  let language: Language = SUPPORTED_LANGUAGES[0]

  if (typeof languageHeader === "string" && SUPPORTED_LANGUAGES.includes(languageHeader)) {
    language = languageHeader
  }

  // extending the Request object with a language property of type Language...

  return next()
}

These are just two examples, but there are several other scenarios where extending Request with custom data can save you time and make your codebase more elegant and maintainable.

Extending the Express Request interface locally

The first idea you might come up with to achieve code that is easier to maintain, is to extend the Request type locally. You can extend the Express Request interface for a single request, like so:

import { NextFunction, Request, Response } from "express"

export type Language = "en" | "es" | "it

export interface LanguageRequest extends Request {
  language: Language
}

export const HelloWorldController = {
    default: async (req: LanguageRequest, res: Response, next: NextFunction) => {
        let message

        switch (req.language) {
            default:
            case "en": {
                message = "Hello, World!"
                break
            }
            case "es": {
                message = "¡Hola, mundo!"
                break
            }
            case "it": {
                message = "Ciao, mondo!"
                break
            }
        }

        res.json(message)
    },
    // other requests...
}

This is an effective solution. As you can see, the default() request has access to the custom language property. You can use it if you need to use a special Request type on a limited amount of requests.

At the same time, keep in mind that this approach involves boilerplate code and can easily lead to code duplication. You could end up duplicating the type extension logic on several controllers, for example. This is something you’ll want to avoid.

For this reason, you should consider extending Request globally. Let’s learn how to do it!

Extending the Express Request type globally in TypeScript

Adding extra fields to the Express Request type definition only takes a few lines of code. Every request in your app will be able to see the new extended type. Let’s see how to globally extend Request and take advantage of the extended type through a demo application based on the middlewares presented earlier.

Clone the GitHub repository that supports the article and launch the sample backend application locally with the following commands:

git clone https://github.com/Tonel/extend-express-request-ts-demo
cd extend-express-request-ts-demo
npm i
npm start

Now, follow along to learn how to take advantage of the extended Express Request type in TypeScript.



Prerequisites

In order to replicate the article’s goal, you’ll need the following:

  • Express >= 4.x
  • @types/express >= 4.x
  • TypeScript >= 4.x

If you do not have an Express project in TypeScript, you can learn how to set up an Express and TypeScript project from scratch here.

Understanding declaration merging in TypeScript

Request is part of the Express types and is used in Express functions, so you do not have control over it. Fortunately, TypeScript supports declaration merging.

Thanks to declaration merging, the TypeScript compiler can merge two or more different declarations that have the same name into a single definition. This means that the resulting definition has all the features of the original declarations.

In other words, declaration merging allows you to add additional properties and methods to an existing type, which is exactly what you want to achieve here.

Let’s look at an example to better understand how declaration merging works:

// declaring an interface with 
// a single property
interface Person {
  fullName: string;
}

// declaring the Person interface again,
// this time with a different property
interface Person {
  age: number;
}

// the resulting Person interface has both 
// the 'fullName' and 'age' properties
const person: Person = {
  fullName: 'Maria Smith',
  age: 37
}

Now, let’s see how to use declaration merging to add custom properties to the Express Request type.

Adding custom properties to the Request type

All you have to do to extend the Request type is define an index.d.ts file as follows:

// src/types/express/index.d.ts

import { Language, User } from "../custom";

// to make the file a module and avoid the TypeScript error
export {}

declare global {
  namespace Express {
    export interface Request {
      language?: Language;
      user?: User;
    }
  }
}

Place this file in the src/types/express folder. TypeScript uses the .d.ts declaration files to load type information about a library written in JavaScript. Here, the index.d.ts global module will be used by TypeScript to extend the Express Request type globally through declaration merging. According to the Express source code, this is the officially endorsed way to extend the Request type.

Now, all your Express requests will be able to access the extended Request type!

Note that the Language and User custom types are defined in the src/types/custom.ts file as below:

// src/types/custom.ts

export const SUPPORTED_LANGUAGES = ["en", "es", "it"]
// this syntax is equals to "en" | "es" | "it"
export type Language = typeof SUPPORTED_LANGUAGES[number]

export type User = {
    id: number,
    name: string,
    surname: string,
    authenticationToken?: string | null
}

These types will be used in the handleCustomLanguageHeader() and handleTokenBasedAuthentication() middleware functions, respectively. Let’s see how.

Using the extended Request object

Now, let’s learn how you can employ the extended Request object. First, let’s complete the middleware functions introduced earlier. This is what authentication.middleware.ts looks like:

// src/middlewares/authentication.middleware.ts 

import { Request, Response, NextFunction } from "express"
import { User } from "../types/custom"

// in-memory database
const users: User[] = [
    {
        id: 1,
        name: "Maria",
        surname: "Williams",
        authenticationToken: "$2b$08$syAMV/CyYt.ioZ3w5eT/G.omLoUdUWwTWu5WF4/cwnD.YBYVjLw2O",
    },
    {
        id: 2,
        name: "James",
        surname: "Smith",
        authenticationToken: null,
    },
    {
        id: 3,
        name: "Patricia",
        surname: "Johnson",
        authenticationToken: "$2b$89$taWEB/dykt.ipQ7w4aTPGdo/aLsURUWqTWi9SX5/cwnD.YBYOjLe90",
    },
]

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
    const authenticationToken = req.headers["authorization"]

    if (authenticationToken !== undefined) {
        // using the in-memory sample database to verify if authenticationToken is valid
        const isTokenValid = !!users.find((u) => u.authenticationToken === authenticationToken)

        if (isTokenValid) {
            // retrieving the user associated with the authenticationToken value
            const user = users.find((u) => u.authenticationToken === authenticationToken)

            req.user = user

            // moving to the next middleware
            return next()
        }
    }

    // if the authorization token is invalid or missing returning a 401 error
    res.status(401).send("Unauthorized")
}

For the sake of simplicity, the authentication token is validated through an in-memory database. In a real-world scenario, replace this simple logic with some queries or API calls. Notice how you can now assign the user associated with the token to the Request custom user property.

Also, note that you extended the Request interface, not the object itself. So, when dealing with a Request property with a structured type, such as the user property, any attempt to directly access its fields will result in an error:

// src/middlewares/authentication.middleware.ts 

// ...

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
    // ...

    // this will not work because user is not defined 
    req.user.id= 1
    req.user.name = "John"    
    req.user.surname = "Williams"       

    // ...
}

This snippet will return a Object is possibly 'undefined' error. This is because user has not been defined yet. In other words, req.user is undefined and you cannot access its inner properties.


More great articles from LogRocket:


If you want to give values to the inner fields of a nested property, you can use the spread operator as follows:

// src/middlewares/authentication.middleware.ts 

// ...

export function handleTokenBasedAuthentication(req: Request, res: Response, next: NextFunction) {
    // ...

    // this line code of code will work as expected
    req.user = { ...req.user, id: 1, name: "John", surname: "Williams" }       

    // ...
}

This code will compile without errors. In detail, the spread operator allows you to overwrite the properties of the req.user object. Since req.user is undefined, the spread operator will treat req.user as an empty object. So, considering that all the required fields by the User type have been provided on the object to the right of the assignment, req.user can be assigned correctly.

Now, let’s see the final language.middleware.ts middleware file:

// src/middlewares/headers.middleware.ts

import { Request, Response, NextFunction } from "express"
import { Language, SUPPORTED_LANGUAGES } from "../types/custom"

export function handleLanguageHeader(req: Request, res: Response, next: NextFunction) {
    const languageHeader = req.headers["content-language"]

    // default language: "en"
    let language: Language = SUPPORTED_LANGUAGES[0]

    if (typeof languageHeader === "string" && SUPPORTED_LANGUAGES.includes(languageHeader)) {
        language = languageHeader
    }

    req.language = language

    return next()
}

The Request custom language property is used to store the Language information.

Next, let’s see the Request custom properties in action in two different APIs. This is what the HelloWorldController object looks like:

// src/controllers/helloWorld.ts 

import { NextFunction, Request, Response } from "express"

export const HelloWorldController = {
    default: async (req: Request<never, never, never, never>, res: Response, next: NextFunction) => {
        let message

        switch (req.language) {
            default:
            case "en": {
                message = "Hello, World!"
                break
            }
            case "es": {
                message = "¡Hola, mundo!"
                break
            }
            case "it": {
                message = "Ciao, mondo!"
                break
            }
        }

        res.json(message)
    },
    hello: async (req: Request<never, never, never, never>, res: Response, next: NextFunction) => {
        res.json(`Hey, ${req.user?.name}`)
    },
}

As you can see, HelloWorldController defines two APIs. The first one uses the Request custom language property, while the second employs the Request custom user property.

Finally, you have to register the middleware functions with their endpoints in the router file as below:

// src/routes/index.ts

import { Router } from "express"
import { HelloWorldController } from "../controllers/helloWorld"
import { handleLanguageHeader } from "../middleware/customHeaders.middleware"
import { handleTokenBasedAuthentication } from "../middleware/authentication.middleware"

export const router = Router()

router.get("/", handleLanguageHeader, HelloWorldController.default)
router.get("/hello", handleTokenBasedAuthentication, HelloWorldController.hello)

Testing the extended Request

Let’s test the APIs defined earlier with curl. First, launch the demo Express application with npm run start.

Now, let’s have a look at the behavior of the / endpoint.

curl -i -H "Content-Language: it" http://localhost:8081/ returns Ciao, mondo!
curl -i http://localhost:8081/ returns the default Hello, World!
curl -i -H "Content-Language: es" http://localhost:8081/ returns ¡Hola, mundo!

As you can see, the language of the message returned by the API returns by the Content-Languageheader as expected.

Now, let’s test the /hello endpoint.

curl -i http://localhost:8081/hello returns a 401 Unauthorized error
curl -i -H "Authorization: $2b$08$syAMV/CyYt.ioZ3w5eT/G.omLoUdUWwTWu5WF4/cwnD.YBYVjLw2O"http://localhost:8081/hello returns¡Hola, Maria!

Again, the response depends on the user loaded thanks to the Authorization header value.
Et voila! You just learned how to extend the Express Request type and how to use it to provide custom information to the controllers.

Conclusion

In this article, we investigated what the Express Request object is, why it is so important, and when you might need to extend it. The Request object stores all the info about the HTTP request. Being able to extend it represents an effective way to pass custom data to controllers directly. As shown, this allows you to avoid code duplication.

Here, you learned how to extend the Express Request type in TypeScript. This is easy and requires only a few lines of code. Also, it can bring several advantages to your backend application, as shown through the sample demo Express application developed in TypeScript.

Thanks for reading! I hope you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.

: Full visibility into your web and mobile apps

LogRocket Dashboard Free Trial Banner

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.
Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Leave a Reply