Build a REST API with Node.js, Mongoose and TypeScript — part 3

Richard Chou
3 min readJan 13, 2023

--

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)

--

--

Richard Chou

I write about Ruby, Rails, AWS and JavaScript. Occasionally other things.