Zod Migration Guide
This guide is for applications that already use drizzle-zod and want a
clearer Nest HTTP boundary without adopting a new package API. The target shape
is the app-owned bridge from the Zod OpenAPI Bridge
page:
- Drizzle tables remain the database source of truth.
drizzle-zodderives runtime request validation.- Explicit Swagger DTO classes document the public API contract.
- Controllers make the validation and documentation choices visible.
There is no nest-drizzle-native/zod import in this migration. Keep the bridge
inside the application until repeated samples prove that a tiny public helper is
worth adding.
1. Install App-Owned Dependencies
npm i drizzle-zod zod @nestjs/swagger
These dependencies belong to the application or sample that uses the bridge.
They are not required for normal nest-drizzle-native usage.
2. Move Validation Into A Named Schema File
Before:
import { createInsertSchema } from 'drizzle-zod';
import { supportTickets } from '../schema';
export const createTicketSchema = createInsertSchema(supportTickets);
After:
import { createInsertSchema } from 'drizzle-zod';
import { z } from 'zod';
import { supportTickets } from '../schema';
export const createTicketSchema = createInsertSchema(supportTickets)
.omit({
id: true,
createdAt: true,
})
.extend({
title: z.string().trim().min(3).max(120),
requesterEmail: z.email(),
priority: z.enum(['low', 'normal', 'urgent']),
estimatePoints: z.int().min(1).max(13),
})
.strict();
export const createTicketInputKeys = createTicketSchema.keyof().options;
export type CreateTicketInput = z.infer<typeof createTicketSchema>;
Use .omit() for database-owned fields and .strict() for route inputs that
should reject undocumented properties.
3. Keep Swagger DTOs Explicit
Before:
@Post()
create(@Body() body: CreateTicketInput) {
return this.ticketsService.create(body);
}
After:
export class CreateTicketDto {
@ApiProperty({ minLength: 3, maxLength: 120 })
title!: string;
@ApiProperty({ format: 'email' })
requesterEmail!: string;
@ApiProperty({ enum: ['low', 'normal', 'urgent'] })
priority!: 'low' | 'normal' | 'urgent';
@ApiProperty({ minimum: 1, maximum: 13 })
estimatePoints!: number;
}
The DTO class is intentionally separate from the Zod schema. That makes the OpenAPI contract easy to inspect in a normal Nest project and keeps the package from forcing a validation ecosystem.
4. Validate In The Controller
@Post()
@UsePipes(new ZodValidationPipe(createTicketSchema))
@ApiBody({ type: CreateTicketDto })
@ApiCreatedResponse({ type: TicketDto })
@ApiBadRequestResponse({ description: 'Zod validation failed' })
create(@Body() body: CreateTicketInput): Promise<TicketDto> {
return this.ticketsService.create(body);
}
The controller shows the two contracts side by side:
ZodValidationPipe(createTicketSchema)validates runtime input.@ApiBody({ type: CreateTicketDto })documents the public request body.
5. Add Contract Tests
Add smoke coverage before migrating many endpoints:
- Invalid payloads are rejected before repository writes.
- Valid payloads persist through real Drizzle queries.
- OpenAPI request properties match
createTicketInputKeys. - Response DTOs do not expose secrets, hashes, or internal database columns.
Run the focused sample as a reference:
npm run test --workspace nest-drizzle-native-sample-13-zod-openapi-bridge
6. Repeat Endpoint By Endpoint
Keep each PR small:
- One feature module, or a few closely related DTOs.
- No unrelated repository refactors.
- No package-level helper extraction during the migration.
- A smoke test or OpenAPI assertion for every public route shape.
When the same local glue appears in multiple focused samples, write a post-sample review. Only then consider whether a small optional helper belongs in the package.