Zod Diff Examples
These examples show how to move from scattered raw drizzle-zod usage toward
the app-owned bridge. The Drizzle schema does not change. The controller makes
validation explicit, and Swagger DTOs keep the public API contract readable.
Basic Create DTO
Before:
import { createInsertSchema } from 'drizzle-zod';
import { users } from '../schema';
export const createUserSchema = createInsertSchema(users);
export type CreateUserInput = z.infer<typeof createUserSchema>;
After:
import { createInsertSchema } from 'drizzle-zod';
import { z } from 'zod';
import { users } from '../schema';
export const createUserSchema = createInsertSchema(users)
.omit({
id: true,
passwordHash: true,
createdAt: true,
updatedAt: true,
})
.extend({
password: z.string().min(8).max(128),
})
.strict();
export const createUserInputKeys = createUserSchema.keyof().options;
export type CreateUserInput = z.infer<typeof createUserSchema>;
Swagger stays explicit:
export class CreateUserDto {
@ApiProperty()
name!: string;
@ApiProperty({ format: 'email' })
email!: string;
@ApiProperty({ minLength: 8, maxLength: 128 })
password!: string;
}
Relation Response
Before:
const postWithAuthorSchema = createSelectSchema(posts).extend({
author: createSelectSchema(users),
});
After:
export const postWithAuthorSchema = createSelectSchema(posts)
.omit({
deletedAt: true,
})
.extend({
author: createSelectSchema(users).omit({
passwordHash: true,
}),
});
Document the response with DTO classes that match the public shape:
export class AuthorDto {
@ApiProperty()
id!: number;
@ApiProperty()
name!: string;
@ApiProperty({ format: 'email' })
email!: string;
}
export class PostWithAuthorDto {
@ApiProperty()
id!: number;
@ApiProperty()
title!: string;
@ApiProperty({ type: AuthorDto })
author!: AuthorDto;
}
Many-To-Many Input
Before:
const createPostSchema = createInsertSchema(posts);
After:
export const createPostSchema = createInsertSchema(posts)
.omit({
id: true,
createdAt: true,
})
.extend({
tagIds: z.array(z.number().int().positive()).min(1),
})
.strict();
export type CreatePostInput = z.infer<typeof createPostSchema>;
Use a request DTO that documents the application-level relation input rather than the join table:
export class CreatePostDto {
@ApiProperty()
title!: string;
@ApiProperty()
content!: string;
@ApiProperty({ type: Number, isArray: true, minimum: 1 })
tagIds!: number[];
}
The repository can then write the post and join rows in a transaction. Keep values parameterized through Drizzle and avoid raw string interpolation.
Controller Diff
Before:
@Post()
create(@Body() body: CreatePostInput) {
return this.postsService.create(body);
}
After:
@Post()
@UsePipes(new ZodValidationPipe(createPostSchema))
@ApiBody({ type: CreatePostDto })
@ApiCreatedResponse({ type: PostWithTagsDto })
@ApiBadRequestResponse({ description: 'Zod validation failed' })
create(@Body() body: CreatePostInput) {
return this.postsService.create(body);
}
The route now shows the complete contract: Zod validates input, Swagger DTOs document the request and response, and the service receives typed data.