Editor’s note: This article was last updated by Yan Sun on 8 July 2024 to cover advanced integration techniques, including how to integrate extended request objects with middleware like express-validator and body-parser. It now also includes a section about handling errors with extended request types.
The Request object plays a critical role in Express. It carries data about every HTTP request from a client to our Node.js server. The data from Request object serves as the foundation for the application logic.
Several properties are provided out-of-the-box by the Request object. But what if we need more than the standard details provided? JavaScript allows us to define new properties directly on the object itself. However, in TypeScript, to ensure type safety, we need to take a different approach: extending the Request type with custom properties.
In this article, we will learn what Request is in Express, and explore why extending the Request type in TypeScript can be useful. Then, we’ll look at how we can take advantage of the extended Request object through a demo Express application built with TypeScript. In summary, we will learn how to extend the Request type in TypeScript to make its instances store custom data we can use at the controller level.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Request object in Express?In Express, the Request object represents the HTTP request sent by a client to an Express server. In other words, an Express server can read the data received from the client through instances of the Request object.
Therefore, Request has several properties to access all the information contained in the HTTP request, but the most important properties are as follows:
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.post("/user", (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
Request?Express controllers can access all the data 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 middleware. Express middleware are functions that can be used to add application- or router-level functionality.
The middleware functions are associated with the endpoints at the router level, as shown below:
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 Express middleware can offer here.
It is important to notice that middleware can modify the Express Request object, adding custom information to make it available at the controller level. For example, let’s say we want to make the APIs available only to users with a valid authentication token. To achieve this, we 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 our service. In other words, the authentication token allows us to identify the user 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.
Suppose several controller-level functions need to know who the user who performed the API call is. Currently, retrieving the user from the Authorization header requires duplicating logic throughout the codebase. To address this, we can extend the Express Request object with a user property populated in the authentication middleware below.
Notice that the Express Request type in TypeScript does not involve a user property. This means that we 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, we can use an extended Request to avoid type casting at the controller level and make the codebase cleaner and easier to manage.
Let’s assume our backend application supports only three languages: English, Spanish, and Italian. In other words, we 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, we 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 we want to use the Content-Language header value as a string, we have to cast it as follows:
const language = (req.headers["content-language"] || "en") as string | undefined
However, filling the code with this logic is not an ideal solution. Instead, we can use a middleware to extend Request, as shown 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 as string))) {
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 us time and make the codebase more elegant and maintainable.
Request interface locallyThe first idea you might come up with to achieve more maintainable code is to extend the Request type locally. We 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. We can use it if we need to use a special Request type on a limited amount of requests.
At the same time, remember that this approach involves boilerplate code and can easily lead to code duplication. We could end up duplicating the type extension logic on several controllers, for example. This is something we want to avoid.
For this reason, we should consider extending Request globally.
Request type globally in TypeScriptAdding extra fields to the Express Request type definition only takes a few lines of code. Every request in the app can 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 middleware 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/sunnyy02/extend-request-type cd extend-request-type npm i npm start
Now, we’ll learn how to take advantage of the extended Express Request type in TypeScript.
To follow along the examples in the article, we’ll need the following:
If you don’t have an Express project in TypeScript, learn how to set up an Express and TypeScript project from scratch.
Request is part of the Express types used in Express functions, so we do not have control over it. Fortunately, TypeScript supports declaration merging.
Thanks to declaration merging, the TypeScript compiler can merge two or more declarations with 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 us to add additional properties and methods to an existing type, which is exactly what we 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 explore how to use declaration merging to add custom properties to the Express Request type.
Request typeTo extend the Request type, all we have to do is define an index.d.ts file:
// 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, TypeScript will use the index.d.ts global module 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 our Express requests can access the extended Request type! Note that the Language and User custom types are defined in the src/types/custom.ts file, as shown 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. Let’s see how.
Request objectNow, let’s learn how we 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 database queries or API calls. Notice how we can now assign the user associated with the token to the Request custom user property.
Also, note that we 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 an Object is possibly 'undefined' error. This is because user has not yet been defined. In other words, req.user is undefined, and we can’t access its inner properties.
If we want to give values to the inner fields of a nested property, we can use the spread operator syntax 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 us to overwrite the properties of the req.user object. Because 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 two API endpoints. 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 API endpoints. The first uses the Request custom language property, while the second employs the Request custom user property.
Finally, we have to register the middleware functions with their endpoints in the router file as shown 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)
req.body, req.params, and req.queryAdditionally, we can extend the Express Request object by using req.body, req.params, and req.query in the extended request types.
We often use req.body to extend the Request object for validating a structured payload. In the example below, ICustomRequestBody represents the user details in the request body. Then, we use the interface to validate the req.body in the createUser route handler:
export interface ICustomRequestBody {
userDetails: {
name: string;
email: string;
};
}
createUser: async (req: Request<{}, {}, ICustomRequestBody>, res: Response, next: NextFunction) => {
const { userDetails } = req.body;
if (!userDetails.name || !userDetails.email) {
return next(new MissingPropertyError('userDetails'));
}
res.json({ message: `User ${userDetails.name} added successfully.` });
}
// define the POST user route
router.post('/user',handleLanguageHeader, HelloWorldController.createUser)
For routes with URL parameters, we can extend the Request object with a specific type for req.params to ensure type safety when accessing route parameters. Here, we define an interface ICustomRequestParams extending ParamsDictionary. The new interface ensures that req.params includes a userId string. The getUserById route handler uses this interface to type check the req.params:
import { ParamsDictionary } from "express-serve-static-core";
export interface ICustomRequestParams extends ParamsDictionary {
userId: string;
}
getUserById: async (req: Request<ICustomRequestParams>, res: Response, next: NextFunction) => {
const { userId } = req.params;
if (!userId) {
return next(new MissingPropertyError('userId'));
}
res.json({ message: `User ${userId} retrieved successfully.` });
}
// define the GetUserById route
router.get("/user/:userId", handleLanguageHeader, HelloWorldController.getUserById)
To validate endpoints that accept query parameters for filtering or sorting, we can extend the Request object for req.query.
In the example below, we define an ICustomRequestQuery interface, which extends Query type. The interface ensures that req.query (typed as Query) includes search and sortBy strings. Then, the searchUsers route handler uses this interface to validate the req.query:
import { Query } from "express-serve-static-core";
export interface ICustomRequestQuery extends Query {
search: string;
sortBy: string;
}
searchUsers: async (req: Request<{}, {}, {}, ICustomRequestQuery>, res: Response, next: NextFunction) => {
const { search, sortBy } = req.query;
if (!search || !sortBy) {
return next(new MissingPropertyError('search or sortBy'));
}
res.json({ message: `Searching for ${search}, sorted by ${sortBy}.` });
}
// define a search user route
router.get("/search", handleLanguageHeader, HelloWorldController.searchUsers)
Handling errors with extended request types ensures our application can manage custom properties safely, and prevents unexpected crashes or incorrect behavior. We can take advantage of the following patterns to handle the errors.
Consider creating a custom error class specifically for errors related to extended request properties. This approach enhances error clarity and simplifies handling.
In the example below, we define a custom error class for missing properties and use it to handle the absence of a custom 'user' property in the hello Express route:
// Define a custom error class
export class MissingPropertyError extends Error {
constructor(public propertyName: string) {
super(`Custom property '${propertyName}' is missing from request object.`);
this.name = 'MissingPropertyError';
}
}
// handle the missing user custom property using the new custom error
hello: async (req: Request<never, never, never, never>, res: Response, next: NextFunction) => {
if (!('user' in req)) {
return next(new MissingPropertyError('user'));
}
res.json(`Hey, ${req.user?.name}`)
}
Another common error handling pattern is type guards. We can use type guards to verify if custom properties exist on the request object to prevent runtime errors.
Here is a type guard function that checks if an object has a 'user' property, and ensures safe usage of the 'user' property in subsequent code:
function isUserRequest(obj: any): obj is Request {
return typeof obj === "object" && obj !== null && "user" in obj;
}
if (isUserRequest(req)) {
// Use customValue safely
} else {
// Handle case where customProperty is not present
}
Express provides a built-in error handler that can be leveraged to catch errors during request processing. We can use it to handle errors related to custom properties within the extended request object.
In the previous example, we pass an error to next(), which isn’t handled in a custom error handler. The built-in error handler will take over and send the error along with the stack trace to the client:
return next(new MissingPropertyError('user'));
We can create a custom error handling middleware for errors related to extended request properties:
function handleExtendedRequestErrors(err: Error, req: Request, res: Response, next: Function) {
if (err instanceof MissingPropertyError) {
res.status(400).json({ message: err.message });
} else {
next(err);
}
}
app.use(handleExtendedRequestErrors);
Here, we define middleware that checks if an error is a MissingPropertyError and responds with a 400 status code if true. Otherwise, we pass the error to the next middleware, and the default error handler will be triggered.
RequestLet’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 returned message depends on the Content-Language header as expected. This is achieved by utilizing the req.language in the route handler.
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 voilà! We just learned how to extend the Express Request type and how to use it to provide custom information to the controllers.
While extending the Request object provides an effective way to manage custom data, integrating it with other middleware can further enhance the Express application’s functionality. Here’s a brief exploration of how extended Request objects work with express-validator and body-parser.
express-validator is a popular library for validating and sanitizing user input in Express applications. It allows us to define validation rules for incoming request body data, to ensure the data meets specified criteria.
Below is a simple example that validates the username field in a POST request to /user, ensuring it is at least five characters long, and returns a 400 status with error details if validation fails:
const { body, validationResult } = require('express-validator');
// skip other plumbing code
app.post('/user', [
// Validation rules
body('username').isLength({ min: 5 }).withMessage('Username must be at least 5 characters long'),
], (req, res) => {
// Handle validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
});
body-parser is a middleware for Express applications used to parse the incoming request bodies, making the data available under req.body. It is commonly used to parse JSON and URL-encoded data.
We can use body-parser with the extended Request object — there’s no conflict. The extended properties will coexist with the parsed request body data on req.body.
Below is an example use case where we configure an Express app to use body-parser, parse incoming JSON, form data from POST requests, and then access it in the route handler:
const bodyParser = require('body-parser');
// Use body-parser middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/data', (req, res) => {
// Access parsed data from the request body
const jsonData = req.body;
res.send(`Received data: ${JSON.stringify(jsonData)}`);
});
In this article, we investigated what the Express Request object is, why it is so important, and when we might need to extend it. The Request object stores all the info about the HTTP request. Extending it represents an effective way to pass custom data to controllers directly. As shown, this allows us to avoid code duplication.
We demonstrated how to extend the Express Request type in TypeScript, which is straightforward and requires only a few lines of code. This approach offers several advantages to the backend application, as illustrated by the sample demo Express application developed in TypeScript.
Thanks for reading! I hope you found this article helpful. Feel free to contact me with any questions, comments, or suggestions.
LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you understand your web and mobile apps — start monitoring for free.

A quick guide to agentic AI. Compare Autogen and Crew AI to build autonomous, tool-using multi-agent systems.

Remix 3 ditches React for a Preact fork and a “Web-First” model. Here’s what it means for React developers — and why it’s controversial.

Compare the top AI development tools and models of November 2025. View updated rankings, feature breakdowns, and find the best fit for you.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the November 5th issue.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now