Type-safe Express validations with Zod
Tags: express, typescript, zod
Published: 2023-04-25 11:46:19

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](https://github.com/romeerez/express-ts-handler) 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](https://github.com/colinhacks/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:

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

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:

```ts
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 });
  }
);
```

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](https://github.com/RobinTail/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](https://github.com/romeerez/express-ts-handler) solves all of the mentioned problems:

```ts
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 }
    },
  }),
);
```

Let me know what you think about it!