Type-safe Express validations with Zod
Roman K
Posted on April 25, 2023
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 });
}
);
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 });
}
);
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 }
},
}),
);
Let me know what you think about it!
Posted on April 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.