Skip to content

Using Typescript with Express Middlewares

Published: at 06:00 PM

My general philosophy when writing typescript code is to avoid defining types as much as possible. From the couple of years of experience I have with it, I’ve noticed that the fewer human defined types are used in a codebase, the more robust it’s type hints are. Following this principle, I tried to write an express middleware that would validate the user inputs and then hoped that the route handler would automatically know the types of the user input. However turns out that is not how it works. If you’re in a similar situation, here are a few things I tried out. Feel free to take inspiration from them.

The Setup

I usually use zod to maintain typesafety during runtime in my codebase. I either use zod as the source of truth for all my types or use an ORM to write database schemas as source of truth and then generate zod schemas from them. In the project, I wrote a higher order function that would return a middleware function. The higher order function would take a zod schema as input and create a function that would validate the req.body based on that. Then it would return the function.

export const validateBodyInput = (schema: ZodSchema) => {
  const validate = (req: Request, res: Response, next: NextFunction) => {
    const d = schema.safeParse(req.body);

    if (d.success) {
      req.data = d.data;
      next();
    } else {
      res.status(400).json({
        error: d.error,
      });
    }
  };

  return validate;
};

To use it on a route we would have to write something like this

app.post(
  "/auth/signin",
  validateBodyInput(signinInputSchema),
  async (req, res) => {
    const { email, password } = req.body;
    const r = await signinOwner(email, password);
    res.json(r);
  }
);

By default the email and password here would have type of any. I was hoping it would automatically infer the types since we are already doing the validation inside the middleware. But since that doesn’t happen, I wanted to find the most simplest and least error prone way to define the types. A very obvious way to achieve that would be to typecast. I could either cast the type of req.body inside the handler function like this:

app.post(
  "/auth/signin",
  validateBodyInput(signinInputSchema),
  async (req, res) => {
    const body = req.body as SigninInputType;
    const { email, password } = body;
    const r = await signinOwner(email, password);
    res.json(r);
  }
);

or I could take advantage of the fact that the request is a generic and thus can accept the type of req.body when it’s defined as the type for req

app.post(
  "/auth/signin",
  validateBodyInput(signinInputSchema),
  async (req: Request<{}, {}, { SigninInputType }>, res) => {
    const { email, password } = req.body;
    const r = await signinOwner(email, password);
    res.json(r);
  }
);

both of these would provide the typesafety I wanted within the handler function and since the SigninInputType is inferred from the signinInputSchema, the types should also be consistent.

type SigninInputType = z.infer<typeof signinInputSchema>;

However, there are 2 issues with this approach:

  1. This method fails to handle parameters that are of non-string type. If I pass id of any object as a url param to any route, the route handler will expect it to be a string. If you define the type to be anything else, the type definition will conflict with express’s default interface for the request object.
  2. This only works if the inputs are available directly in either the request body or the url params. If the data requires some sort of processing before it’s sent to services (imagine validating and extracting data from a jwt token), then this method will not work.
app.patch(
  "/owner/property/:id",
  validateBodyInput(updatePropertyDetailsInputSchema),
  validateParamsInput(choosePropertyParamsSchema),
  async (
    req: Request<
      { ChoosePropertyParamsType }, {}, { UpdatePropertyDetailsInputType}
    >,
    res,
    next
  ) => {
    const { id } = req.params
    const { address, rent } = req.body;
    const r = await updateProperty(id, address, rent);
    res.json(r);
  }

The codeblock above illustrates the problem (1). The id expected by the updateProperty is number but values obtained from url params will be of type string. So, I would have to define a custom type for the id where its a string and then within the route handler, convert the type. But that seems very forgettable and thus can cause errors.

To solve both of these problems, we follow a slightly different approach. This alternative approach would seem more natural if we tackled this problem without worrying about types. It’s to do all the processing within the middleware and then pass those values in a custom attribute of the request object. If we try to implement this the issue we will face is that any custom attribute we try to add to the request object will conflict with express’s interface for the request object. To solve this issue we can define the type:

export interface CustomRequest<S = undefined> extends Request {
  data?: S;
}

This interface is completely compatible with the default expectation of the express app’s request object interface. At the same time, it accepts an optional data attribute that can hold any arbitrary schema. The schema of the data will be passed whenever CustomRequest interface is used. Using it would look something like this:

app.post(
  "/auth/signin",
  validateBodyInput(signinInputSchema),
  async (req: CustomRequest<SigninInputType>, res) => {
    const { email, password } = req.data;
    const r = await signinOwner(email, password);
    res.json(r);
  }
);

The middleware would have to change to look something like this:

export const validateBodyInput = (schema: ZodSchema) => {
  type CustomData = z.infer<typeof schema>;
  interface CustomRequest extends Request {
    data?: CustomData;
  }

  const validate = (req: CustomRequest, res: Response, next: NextFunction) => {
    const d = schema.safeParse(req.body);

    if (d.success) {
      req.data = d.data;
      next();
    } else {
      res.status(400).json({
        error: d.error,
      });
    }
  };

  return validate;
};

The middleware accepts the zod schema, uses it to create a type definition for Custom Request and assigns the interface for the request object of the middleware as that Custom Request interface. With this strategy we have all our processed data into the req.data attribute and when we access the req.data attribute we have the correct type which also matches the expected types derived from our prisma schema and is used in the rest of the application.

This strategy allows us to handle non-string type url parameters because the middleware for processing and validating url parameters will be responsible for converting string type to its respective types if compatible. Within the route handler, we will access data only from the req.data attribute and all the data in that will be typesafe as we passed the type definition of the req.data as argument for the CustomRequest generic.

Personally, I think the express request object should have a data attribute which accepts arbitrary strings as keys and objects as values, or maybe even accepts schemas like the CustomRequest generic does since storing data in the request object while in the middleware is a pretty common pattern. Or maybe that’s my lack of experience with writing typesafe libraries speaking.