From bec46ac54a398d7314c04f6dd156aeb8f0842290 Mon Sep 17 00:00:00 2001 From: Code Date: Sun, 27 Sep 2026 21:45:51 +0100 Subject: [PATCH] Fix #239: Add Zod validation pipe and DTO transformer for uniform request parsing --- src/common/decorators/zod-body.decorator.ts | 7 +++++++ src/common/decorators/zod-param.decorator.ts | 7 +++++++ src/common/decorators/zod-query.decorator.ts | 7 +++++++ src/common/pipes/index.ts | 4 ---- src/common/pipes/zod-validation.pipe.spec.ts | 2 +- src/modules/example/example.controller.ts | 11 +++++++++++ src/modules/example/example.dto.ts | 9 +++++++++ 7 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/common/decorators/zod-body.decorator.ts create mode 100644 src/common/decorators/zod-param.decorator.ts create mode 100644 src/common/decorators/zod-query.decorator.ts create mode 100644 src/modules/example/example.controller.ts create mode 100644 src/modules/example/example.dto.ts diff --git a/src/common/decorators/zod-body.decorator.ts b/src/common/decorators/zod-body.decorator.ts new file mode 100644 index 00000000..be71c6a5 --- /dev/null +++ b/src/common/decorators/zod-body.decorator.ts @@ -0,0 +1,7 @@ +import { Body } from '@nestjs/common'; +import { ZodType } from 'zod'; +import { ZodValidationPipe, ZodValidationPipeOptions } from '../pipes/zod-validation.pipe'; + +export function ZodBody(schema: ZodType, options?: ZodValidationPipeOptions) { + return Body(new ZodValidationPipe(schema, options)); +} diff --git a/src/common/decorators/zod-param.decorator.ts b/src/common/decorators/zod-param.decorator.ts new file mode 100644 index 00000000..9c87d892 --- /dev/null +++ b/src/common/decorators/zod-param.decorator.ts @@ -0,0 +1,7 @@ +import { Param } from '@nestjs/common'; +import { ZodType } from 'zod'; +import { ZodValidationPipe, ZodValidationPipeOptions } from '../pipes/zod-validation.pipe'; + +export function ZodParam(schema: ZodType, options?: ZodValidationPipeOptions) { + return Param(new ZodValidationPipe(schema, options)); +} diff --git a/src/common/decorators/zod-query.decorator.ts b/src/common/decorators/zod-query.decorator.ts new file mode 100644 index 00000000..aec7c286 --- /dev/null +++ b/src/common/decorators/zod-query.decorator.ts @@ -0,0 +1,7 @@ +import { Query } from '@nestjs/common'; +import { ZodType } from 'zod'; +import { ZodValidationPipe, ZodValidationPipeOptions } from '../pipes/zod-validation.pipe'; + +export function ZodQuery(schema: ZodType, options?: ZodValidationPipeOptions) { + return Query(new ZodValidationPipe(schema, options)); +} diff --git a/src/common/pipes/index.ts b/src/common/pipes/index.ts index be7c5d4d..6c856e1f 100644 --- a/src/common/pipes/index.ts +++ b/src/common/pipes/index.ts @@ -1,5 +1 @@ -/** - * Barrel for the shared pipes. Prefer importing from `@common/pipes` (or this - * path) so a pipe is added in exactly one place and every consumer picks it up. - */ export * from './zod-validation.pipe'; diff --git a/src/common/pipes/zod-validation.pipe.spec.ts b/src/common/pipes/zod-validation.pipe.spec.ts index 687b588c..2c1cdec8 100644 --- a/src/common/pipes/zod-validation.pipe.spec.ts +++ b/src/common/pipes/zod-validation.pipe.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { ArgumentMetadata } from '@nestjs/common'; import { z } from 'zod'; import { ZodValidationPipe } from './zod-validation.pipe'; diff --git a/src/modules/example/example.controller.ts b/src/modules/example/example.controller.ts new file mode 100644 index 00000000..19d675fc --- /dev/null +++ b/src/modules/example/example.controller.ts @@ -0,0 +1,11 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe'; +import { createUserSchema, CreateUserDto } from './example.dto'; + +@Controller('example') +export class ExampleController { + @Post() + createUser(@Body(new ZodValidationPipe(createUserSchema)) dto: CreateUserDto): CreateUserDto { + return dto; + } +} diff --git a/src/modules/example/example.dto.ts b/src/modules/example/example.dto.ts new file mode 100644 index 00000000..7705758d --- /dev/null +++ b/src/modules/example/example.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const createUserSchema = z.object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email address'), + age: z.number().int().positive().optional(), +}); + +export type CreateUserDto = z.infer;