Build a REST API with Node.js, Mongoose and TypeScript — part 3
In this post, I will explain how to use Zod to validate requests to Node.js REST API.
Usage
A user has 3 attributes: name, email and password. We can create a user schema:
user.schema.ts
import { object, string, TypeOf } from "zod";
const createUserInputSchema = {
body: object({
name: string({
required_error: "name is required",
}),
password: string({
required_error: "password is required",
}).min(12, "password too short - should be 12 chars minimum"),
email: string({
required_error: "email is required",
}).email("not a valid email"),
})
}
const createUserInput = object(createUserInputSchema)
export type CreateUserInput = TypeOf<typeof createUserInput>
In createUserHandler, we can cast the request body to CreateUserInput:
import { CreateUserInput } from "../schema/user.schema";
export async function createUserHandler(
req: Request<{}, {}, CreateUserInput["body"]>,
res: Response,
) {
try {
const user = await createUser(req.body);
....
}
....
}
Then TypeScript will know the shape of the request body.
When we create such user through our API (POST /api/users), we want to make sure user has the right password. So we will include an extra field passwordConfirmation in the request. It must be the same as password. passwordConfirmation is not a field on user.
We can do the checking before createUserHandler is called. Middleware is a good place for the checking.
We will extend the createUserInputSchema to include passwordConfirmation. We also check passwordConfirmation equals to password.
const userInputSchema = createUserInputSchema.body.extend({
passwordConfirmation: string({
required_error: "passwordConfirmation is required",
}),
});
const refinedUserInputSchema = userInputSchema.refine((data) => data.password === data.passwordConfirmation, {
message: "passwords do not match",
path: ["passwordConfirmation"],
})
export const createUserSchema = object({ body: refinedUserInputSchema });
Then we will create a validator. The validator will run validation based on a schema, in our case createUserSchema. So the validator will check name, email, password are present, passwordConfirmation equals to password…and so on.
validateResource.ts
import { Request, Response, NextFunction } from "express";
import { AnyZodObject } from "zod";
const validate =
(schema: AnyZodObject) =>
(req: Request, res: Response, next: NextFunction) => {
try {
schema.parse({
body: req.body,
query: req.query,
params: req.params,
})
next();
} catch (e: any) {
return res.status(400).send(e.errors);
}
};
export default validate;
We then place the validator before createUserHandler.
routes.ts
app.post("/api/users", validateResource(createUserSchema), createUserHandler);
Testing
When passwords not matching in request:
When email is missing in request:
When name is missing in request:
User is created when all validations are passed:
This concludes the series of Build a REST API with Node.js, Mongoose and TypeScript. Thanks for reading.
Developers and entrepreneurs:
Do you have a business idea, and want to run it on AWS, but don’t know how?
This ebook will show you how to build a full web-stack from scratch, with AWS Copilot.
Focus your time on the business side of things instead of connecting AWS resources.
(Source code included)