Question

Extend the Express Request Object with TypeScript

typescriptnode.jsexpress

Question

How can I add a custom property to the Express Request object from middleware in TypeScript, so that I can use dot notation such as req.property rather than bracket notation?

app.use((req, res, next) => {
  req.property = setProperty();
  next();
});

Short Answer

You will learn how TypeScript declaration merging extends Express's Request type, how middleware adds the value at runtime, and how to ensure the property is available before a route uses it.

Concept

Express request objects are ordinary JavaScript objects. A middleware can attach a new property at runtime:

req.property = setProperty();

However, TypeScript checks code against type definitions. Express's built-in Request type does not know that property exists, so TypeScript reports an error even though JavaScript would allow the assignment.

The solution is declaration merging. You add a type declaration that tells TypeScript: “Every Express request may include this property.” TypeScript combines your interface declaration with Express's existing Request interface.

This has two separate parts:

  1. Type declaration: makes req.property valid to TypeScript.
  2. Middleware assignment: actually puts a value on the request while the application runs.

The declaration does not create the property by itself. Middleware does.

Mental Model

Think of an Express request as a delivery box moving through a conveyor belt of middleware.

  • Middleware can place a label or item in the box: req.property = value.
  • TypeScript is the inventory system that tracks what a box is allowed to contain.
  • Declaration merging updates the inventory system so it recognizes the new item.

A later route can safely use the item only if the middleware that adds it ran earlier on the conveyor belt.

Take Quiz

Syntax and Examples

Create a declaration file, for example src/types/express.d.ts:

export {};

declare global {
  namespace Express {
    interface Request {
      property?: string;
    }
  }
}

Then add the value in middleware:

import express from "express";

const app = express();

function setProperty(): string {
  return "value created by middleware";
}

app.use((req, res, next) => {
  req.property = setProperty();
  next();
});

app.get("/", (req, res) => {
  res.json({ property: req.property });
});

property?: string means the property is optional. This is usually accurate because not every request is guaranteed to pass through the middleware.

If a route is guaranteed to run only after the middleware, you can declare it as required:

interface Request {
  property: string;
}

Use a meaningful name in real applications, such as req.user, req.requestId, or req.tenant.

Step by Step Execution

Consider this request flow:

app.use((req, res, next) => {
  req.requestId = "req-123";
  next();
});

app.get("/orders", (req, res) => {
  res.json({ id: req.requestId });
});

Assuming requestId was declared on Express.Request, this is what happens:

  1. A client requests GET /orders.
  2. Express creates a request object for this request.
  3. The first middleware runs and assigns "req-123" to req.requestId.
  4. next() passes the same request object to the next matching handler.
  5. The /orders route receives that same object, including requestId.
  6. The route returns { "id": "req-123" }.

If the route were registered before the middleware, or mounted outside the middleware's router path, req.requestId might not have been assigned yet.

Real World Use Cases

Custom request properties are useful for data that middleware prepares for later handlers:

  • Authentication: attach the authenticated user as req.user.
  • Request tracing: attach a unique req.requestId for logs and error reports.
  • Multi-tenant applications: attach the selected req.tenant after reading the host name or token.
  • Locale selection: attach req.locale after parsing headers or cookies.
  • Authorization context: attach permissions or roles after validating an access token.

For example, a request ID middleware can make all logs for one HTTP request easy to find:

app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  next();
});
Take Quiz

Real Codebase Usage

In production code, developers usually combine declaration merging with middleware ordering and defensive checks.

Authentication middleware

app.use((req, res, next) => {
  const user = findUserFromToken(req.headers.authorization);

  if (!user) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }

  req.user = user;
  next();
});

Protected route with a guard clause

Even when a type declares user as optional, a route can validate it before use:

app.get("/profile", (req, res) => {
  if (!req.user) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }

  res.json({ name: req.user.name });
});

Prefer scoped middleware when appropriate

Authentication should often apply only to protected endpoints:

app.use("/api", authenticate);
app.get("/api/profile", profileHandler);

This makes the request flow clearer: routes under /api receive the value after authenticate succeeds.

For data that should not be attached to the HTTP request, consider Express's res.locals. It is designed for values shared across middleware and response rendering, but it is accessed as res.locals.value, not req.value.

Common Mistakes

Adding the value without extending the type

This runs in JavaScript but fails TypeScript checking:

app.use((req, res, next) => {
  req.property = "hello"; // Property 'property' does not exist on type 'Request'
  next();
});

Add a .d.ts declaration file rather than using as any.

Using as any to silence the error

(req as any).property = "hello";

This removes useful type checking. A typo such as req.proprety would no longer be caught. Declaration merging preserves dot notation and type safety.

Forgetting that the property may be missing

If the declaration uses property?: string, this is unsafe:

const upper = req.property.toUpperCase();

Use a guard or a fallback:

const upper = req.property?.toUpperCase() ?? "UNKNOWN";

Registering middleware after the route

app.get("/", handler);
app.use(addProperty); // Too late for the route above

Register addProperty before any route that needs its value.

Placing the declaration outside TypeScript's project

Make sure the .d.ts file is included by tsconfig.json. If your configuration has an include list, include the source directory containing the declaration:

{
  "include": ["src"]
}

Restart the TypeScript server or development process after adding a new declaration file if the editor does not recognize it immediately.

Comparisons

ApproachWhat it doesWhen to use it
Declaration mergingExtends Express's global Request typeA property is used across multiple handlers or files
req as anyDisables checking for that accessAvoid; it hides mistakes
Local intersection typeTypes one particular handler's requestUseful for narrowly scoped code, but less convenient across an app
res.localsStores request-scoped values on the response objectValues intended for later middleware or template rendering
Passing function argumentsMakes dependencies explicitBusiness logic outside Express handlers

A local type can look like this:

type RequestWithProperty = Express.Request & {
  property: string;
};

But a handler typed with RequestWithProperty does not make Express automatically provide the property. You must still assign it in middleware. For application-wide request data, declaration merging is generally clearer.

Take Quiz

Cheat Sheet

// src/types/express.d.ts
export {};

declare global {
  namespace Express {
    interface Request {
      requestId?: string;
    }
  }
}
// Add the runtime value before dependent routes
app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  next();
});
// Safely read an optional property
if (req.requestId) {
  console.log(req.requestId);
}

Rules:

  • A .d.ts file changes TypeScript's knowledge, not the runtime object.
  • Middleware must assign the property before another handler reads it.
  • Use ?: unless every relevant request is guaranteed to have the value.
  • Keep request property names specific, such as user, tenant, or requestId.
  • Ensure the declaration file is included by tsconfig.json.

FAQ

How do I add a property to Express Request in TypeScript?

Create a .d.ts file that augments Express.Request, then assign the property in middleware. Declaration merging enables typed dot notation such as req.user.

Why does TypeScript say a property does not exist on Request?

Express's original type definitions do not include your custom field. TypeScript needs a declaration merge before it accepts the property.

Does declaration merging add the property at runtime?

No. It only changes type information. Your middleware must assign the value during request handling.

Should my custom request property be optional?

Usually, yes. Make it required only when your routing structure guarantees that the assigning middleware always runs first.

Where should the Express declaration file go?

A common location is src/types/express.d.ts. Any location is fine as long as TypeScript includes it in the project.

Can I use req.property without bracket notation?

Yes. After augmenting Express.Request, TypeScript recognizes normal dot notation.

When should I use res.locals instead of extending req?

Use res.locals for values shared during response processing, especially template data. Extend req when the value represents request context such as an authenticated user or request ID.

Take Quiz

Mini Project

Description

Build a request-tracing feature for an Express API. A middleware creates a unique request ID, attaches it to every request, and returns it in both logs and API responses. This is a small version of the tracing context used to investigate production errors.

Goal

Create a typed req.requestId property that is assigned by middleware and used by an API route.

Requirements

Requirement 1 Requirement 2 Requirement 3

Take Quiz

Keep learning

Related questions

View more