Skip to Content
Augment Express Request Without Fighting the Compiler
Advanced
0 likes
Tutorials

Augment Express Request Without Fighting the Compiler

Add a user property to the Express request type with a module augmentation in one declaration file. After this, middleware and handlers agree on the type and you avoid the mistake that floods node_modules with errors.

Skip ahead: If your project already compiles with @types/express and you only want the fix, skip to "The fix: augment express-serve-static-core", then continue.

A middleware reads a token, finds the user and stores it on req.user. The next handler reads req.user. In JavaScript this is routine. In TypeScript the compiler tells you that user does not exist on Request, and the quick fixes ((req as any).user, or a new Request type in every file) spread through the whole project. There is a short, correct fix, and one common way to get it wrong that produces a hundred errors. This page covers both. The packages are real, so you need network access to run it.

Step 1: a small Express project

{
  "name": "exdemo",
  "private": true,
  "type": "module",
  "dependencies": { "express": "5.3.0" },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/express": "5.0.6",
    "@types/node": "26.6.5"
  }
}
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "strict": true,
    "types": ["node"],
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

"types": ["node"] matters on TypeScript 7. The default list of global type packages is empty now, so without it @types/node is not loaded. Install the packages.

npm install --no-audit --no-fund --loglevel=error

Step 2: the error

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

export const app = express();

app.use((req: Request, _res: Response, next: NextFunction) => {
  if (req.header("authorization") === "Bearer ana") {
    req.user = { id: "u_1", name: "Ana" };
  }
  next();
});

app.get("/me", (req, res) => {
  res.json({ user: req.user?.name ?? null });
});
npx tsc -p .
src/app.ts(8,9): error TS2339: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
src/app.ts(14,24): error TS2339: Property 'user' does not exist on type 'Request<{}, any, any, ParsedQs, Record<string, any>>'.

The compiler is right. The Request type comes from a declaration file and has no user. You can see two errors with the same cause: one where you set it and one where you read it.

The fix: augment express-serve-static-core

Express's Request is declared in a package called express-serve-static-core, and the express types extend it. You add your property to the interface there, in a declaration file inside your project.

import "express";

declare module "express-serve-static-core" {
  interface Request {
    user?: { id: string; name: string };
  }
}
npx tsc -p .
echo "tsc exit code $?"
tsc exit code 0

The two errors are gone, and every handler in the project now sees req.user. The property is optional (user?), because a request that has not been through the middleware has no user, and you want the compiler to make you check.

Now run it. A tiny script starts the app on a free port and calls it twice, once without the header and once with it.

import { app } from "./app.js";

const server = app.listen(0, async () => {
  const address = server.address();
  const port = typeof address === "object" && address ? address.port : 0;
  const calls: Record<string, string>[] = [{}, { authorization: "Bearer ana" }];

  for (const headers of calls) {
    const response = await fetch(`http://127.0.0.1:${port}/me`, { headers });
    console.log(await response.text());
  }
  server.close();
});
npx tsc -p .
node dist/run.js
{"user":null}
{"user":"Ana"}

💡 Tip: The import "express" at the top of express.d.ts is not decoration. It is what the next section is about.

The mistake that breaks everything

Delete that first line.

declare module "express-serve-static-core" {
  interface Request {
    user?: { id: string; name: string };
  }
}
npx tsc -p . | head -3
node_modules/@types/express/index.d.ts(18,28): error TS2694: Namespace '"express-serve-static-core"' has no exported member 'Express'.
node_modules/@types/express/index.d.ts(49,55): error TS2740: Type 'Response<any, Record<string, any>>' is missing the following properties from type 'ServerResponse<IncomingMessage>': statusCode, statusMessage, strictContentLength, assignSocket, and 64 more.
node_modules/@types/express/index.d.ts(57,59): error TS2694: Namespace '"express-serve-static-core"' has no exported member 'Router'.

Without an import or an export, the file is a script, not a module. In a script, declare module "..." does not add to an existing module. It defines a new one and replaces the real one, so the Express types lose everything they took from it, and the errors appear in node_modules, in files you never touched. Put the import back and the errors disappear. This is the same rule as for declare global: augmentation only works from inside a module.

import "express";

declare module "express-serve-static-core" {
  interface Request {
    user?: { id: string; name: string };
  }
}

Check that the type is enforced

import type { Request } from "express";

export function setUser(req: Request): void {
  req.user = { id: 1, name: "Ana" };
}
npx tsc -p .
src/bad.ts(4,16): error TS2322: Type 'number' is not assignable to type 'string'.

The id must be a string, as declared. The augmentation is a real type, not a loose property bag.

Common mistakes

  • Augmenting "express" instead of "express-serve-static-core". The Request interface lives in the second one. Augmenting "express" fixed the middleware that imports Request from express, but the handler on app.get, whose req is inferred from the core types, still failed with the same error.
  • Putting the file outside include. Keep it in src, or add its folder.
  • Making the property required. Then every place that builds a request, including tests, must supply a user. Use user? and narrow where needed.
  • Skipping "types": ["node"] and getting a different set of errors about process and Buffer.

âš  Warning: An augmentation is a promise, like a hand-written declaration. If no middleware sets req.user on a route, the compiler cannot know. Keep the middleware that sets it close to the type that describes it, and name them together.

Related course: TypeScript Advanced.

About

Add a user property to the Express request type with a module augmentation in one declaration file. After this, middleware and handlers agree on the type and you avoid the mistake that floods node_modules with errors.

Responsible
Christopher R
Last Update
10/09/2026
Reading Time
5 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.