I was building an API recently when I hit a familiar wall. My TypeScript interfaces were perfect in my IDE, but the moment a real request hit my server, all bets were off. A user could send a string where a number should be, or an array where an object was expected, and my code would break. TypeScript’s safety vanishes at runtime. That’s when I decided to look for a solution that could enforce my data contracts from the moment a request arrives. This led me to combine Nest.js, a framework I love for its structure, with Zod, a tool built for this exact problem.
Why does this matter? Think about the last time an API you built crashed because of unexpected data. It’s frustrating, right? The goal is to catch these errors early, before they can corrupt your database or cause a cascade of failures. By validating data at the very edge of your application, you create a robust first line of defense.
So, how do we start? First, you need both libraries in your project. You can add them with your package manager.
npm install zod
Zod works by letting you define a schema. This schema does two things: it validates data at runtime, and it creates a TypeScript type for you. You define it once, and you get both safety and clarity. Here’s a basic example of a schema for creating a user.
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().positive().optional(),
});
// This type is automatically inferred from the schema!
type CreateUserDto = z.infer<typeof createUserSchema>;
See what happened there? We wrote the validation rules—email format, password length, optional positive age—and TypeScript now knows exactly what shape CreateUserDto should have. There’s no need to write an interface separately and hope it matches your validation later. They are forever in sync.
Now, how do we make Nest.js use this? Nest.js handles requests through “pipes.” A pipe can transform or validate data before it reaches your controller. We can create a custom pipe that uses any Zod schema we give it. This is the glue that holds everything together.
// zod-validation.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
@Injectable()
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown) {
const result = this.schema.safeParse(value);
if (!result.success) {
throw new BadRequestException({
message: 'Validation failed',
errors: result.error.format(),
});
}
return result.data;
}
}
This pipe is reusable. We give it a Zod schema when we use it, and it will validate the incoming data. If the data is invalid, it throws a BadRequestException with details about what went wrong. If it’s valid, it passes the clean, typed data to your controller method.
Let’s use it in a controller. The process becomes clean and declarative.
// users.controller.ts
import { Body, Controller, Post, UsePipes } from '@nestjs/common';
import { ZodValidationPipe } from './zod-validation.pipe';
import { createUserSchema, CreateUserDto } from './schemas/user.schema';
@Controller('users')
export class UsersController {
@Post()
@UsePipes(new ZodValidationPipe(createUserSchema))
createUser(@Body() createUserDto: CreateUserDto) {
// By the time we get here, `createUserDto` is guaranteed to be valid.
console.log('Creating user with data:', createUserDto);
// Your business logic here...
}
}
What’s the benefit of this approach? For one, it’s incredibly consistent. The same schema that validates a request body can validate the response from an external API or the data you’re about to save to a file. It’s a single source of truth. Have you ever updated a DTO class but forgotten to update the corresponding validation decorators? That mistake becomes impossible.
Zod also excels at handling complex, nested data. Imagine an order with a list of items, each with its own set of properties. Defining this with Zod is straightforward and remains easy to read.
const orderItemSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
notes: z.string().max(500).optional(),
});
const createOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(orderItemSchema).nonempty(),
priorityShipping: z.boolean().default(false),
});
type CreateOrderDto = z.infer<typeof createOrderSchema>;
The schema reads almost like plain English. It’s an object with a customerId (which must be a UUID string), a non-empty array of items (each matching the orderItemSchema), and a priorityShipping boolean that defaults to false. The inferred type is just as precise.
This method does more than just say “no” to bad data. It can also clean and transform it. Using .transform(), you can modify a value after it passes validation. For instance, you could ensure an email is always lowercase or convert a string date into a Date object. This keeps your business logic free of data-munging code.
Some might ask, “Doesn’t Nest.js already have a validation system?” It does, using class-validator and decorators. It works well. But the functional, composable nature of Zod appeals to many developers. You can build small, testable schema pieces and combine them like building blocks. There’s no reliance on experimental decorator metadata. Your validation logic is just plain JavaScript that can be run anywhere.
In the end, combining Nest.js and Zod gives you a powerful, type-safe pipeline for data. It catches errors where they start—at the boundary of your application. It reduces duplication and keeps your types and validation rules locked together. The code is easier to reason about and simpler to test. For me, it turned a point of friction into a strength.
What problems could this approach solve in your current project? Have you faced issues where the data shape you expected wasn’t the data shape you received?
I hope this walkthrough gives you a clear path to stronger, more resilient APIs. If you’ve tried similar integrations or have questions about edge cases, I’d love to hear about it in the comments. If you found this useful, please consider sharing it with other developers who might be facing the same data validation challenges.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva