express: extending the request interface in middleware

2021-12-22

 | 

~3 min read

 | 

445 words

One of Express.js’s strengths is how easy it is to write middleware. However, the first time you try to do so with the type bindings for Typescript you might run into some issues.

For example, imagine the middleware function that decodes a JWT and looks up a user in a database. If that user exists, we add them to the request. If they don’t, we respond with a 401 Unauthorized.

One way to write that would look like the following:

protect.ts
import { Request, Response, NextFunction } from "express"
import { verifyToken } from "../../utils"
import { fetchUserById } from "../users/user.service"

export const protect = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  const bearer = req?.headers?.authorization

  if (!bearer || !bearer.startsWith("Bearer ")) {
    return res.status(401).end()
  }

  const token = bearer.split("Bearer ")[1].trim()

  let payload
  try {
    payload = await verifyToken(token)
  } catch (e) {
    return res.status(401).end()
  }

  const user = await fetchUserById(payload.id)

  if (!user) {
    return res.status(401).end()
  }

  req.user = user // Compilation error
  next()
}

The problem is that Typescript will complain about this as there’s no user property on the Request type.

And, while the type for Request accepts generics, none of them allow you to augment the request itself.

So what can we do? We can use module augmentation, a form of declaration merging!

protect.ts
import { Request, Response, NextFunction } from 'express'
import { verifyToken } from '../../utils'
+ import { User } from '../users'
import { fetchUserById } from '../users/user.service'

+ declare module 'express' {
+   export interface Request {
+     user?: User
+   }
+ }
+
+ const payloadHasId = (payload: unknown): payload is { id: string } => {
+   return Boolean(payload && typeof payload === 'object' && 'id' in payload)
+ }
+
export const protect = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  const bearer = req?.headers?.authorization

  if (!bearer || !bearer.startsWith('Bearer ')) {
    return res.status(401).end()
  }

  const token = bearer.split('Bearer ')[1].trim()

  let payload
  try {
    payload = await verifyToken(token)
+     if (!payloadHasId(payload)) {
+       throw new Error('Token missing required information')
+     }
  } catch (e) {
    return res.status(401).end()
  }

  const user = await fetchUserById(payload.id)

  if (!user) {
    return res.status(401).end()
  }

  req.user = user
  next()
}

By declaring a new definition for the module "express" we can extend the interface for Request as we need. In this particular case, we needed user as type User (which we define in the app), but it could have been anything.

The other great part is that because Typescript is aware of this new merged definition, we are able to access the user property on any Request typed variables throughout the app.


Related Posts
  • Typescript: Declaration Merging


  • Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!