Type-safe Express validations with Zod

romeerez

Roman K

Posted on April 25, 2023

Type-safe Express validations with Zod

TypeScript is known for its ability to catch the bugs early when writing the code, rather than letting users to find problems for us in production. But TypeScript has one downside: its checks are easily disabled by any type. When we have any types in our code, TS becomes just a wordy version of JavaScript, and it won't provide any guarantees.

I want to share a library which was inspired by the fact that all or almost all examples you can find on the internet for how to do validations with Express are not type safe, which means we can still make mistakes that aren't caught by TypeScript.

At the moment, my library only supports Zod which is easy to use and it can infer types for us.

To see the problem, try googling "express typescript zod" to see how it's commonly done.

Example from the first link:

app.post("/create",
  validate(dataSchema),
  (req: Request, res: Response): Response => {
    return res.json({ ...req.body });
  }
);
Enter fullscreen mode Exit fullscreen mode

Here req.body is of type any, so we can easily break the code inside of route handler by changing a dataSchema and have a bug in production:

app.post("/create",
  validate(dataSchema),
  (req: Request, res: Response): Response => {
    // access any property, no matter how `dataSchema` looks like
    req.body.one.two.three

    return res.json({ ...req.body });
  }
);
Enter fullscreen mode Exit fullscreen mode

We can use z.infer<typeof dataSchema> to let typescript know that this any type should be treated as dataSchema type. And it actually solves the problem. But this is a workaround, and we can still make a mistake, we can forget to specify this as, we can accidentally write typeof otherSchema, and it's more code to write.

Second link is express-zod-api, it looks similar to my library, but instead of validating req.params, req.query, and req.body, it offers input and output instead. This makes it very different from usual Express workflows.

The rest of google results seems pretty irrelevant, but if you try to search express zod typescript on dev.to you'll also see a lot of post of how to make it in the unsafe way. I didn't find a single example of how make it properly.

When we are making a public API interfaces, it seems to be important to return correct responses. It seems so, but again, you won't find any examples over the internet of how to validate and type-check the response type with Express.

Another issue is req.user which is commonly used for storing a current user record. No matter if you have used the middleware or not, all existing examples have it defined always.

express-ts-handler solves all of the mentioned problems:

app.post('/path/:id',
  handler({
    // middleware is setting req.user
    use: authorizeUser,

    // coerce and validate id from route path
    params: {
      id: z.coerce.number().int(),
    },
    // validate query string
    query: {
      key: z.string().optional(),
    },
    // validate request body
    body: {
      name: z.string(),
    },
    // validate route response
    result: {
      prop: z.boolean(),
    },
    // finally, route handler. It may be sync or async
    async handler(req) {
      // req.user has the exact type which was set by the middleware
      req.user.id

      // all the data is typed properly
      const { id } = req.params // { id: number }
      const { key } = req.query // { key: string }
      const { name } = req.body // { name: string }

      // TS error when trying to return extra fields:
      // return { prop: true, extra: true }

      // returned data is validated when NODE_ENV !== production
      return { prop: true }
    },
  }),
);
Enter fullscreen mode Exit fullscreen mode

Let me know what you think about it!

💖 💪 🙅 🚩
romeerez
Roman K

Posted on April 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related