Skip to content

mag123c/nestjs-stdschema

Repository files navigation

@mag123c/nestjs-stdschema

Universal schema validation for NestJS using the standard-schema specification.

npm version CI License: MIT

Why This Package?

  • One package, any standard-schema validator: Tested with Zod & Valibot, compatible with 20+ validators implementing the spec
  • Zero vendor lock-in: Switch validators without changing your NestJS code
  • Type-safe: Full TypeScript support with automatic type inference
  • OpenAPI ready: Automatic Swagger documentation via @nestjs/swagger integration
  • Minimal footprint: No runtime dependencies on specific validators

Installation

npm install @mag123c/nestjs-stdschema
# or
pnpm add @mag123c/nestjs-stdschema
# or
yarn add @mag123c/nestjs-stdschema

Then install your preferred validator:

# Zod
npm install zod

# Valibot
npm install valibot

# ArkType
npm install arktype

Quick Start

Basic Validation (Route Level)

import { Body, Controller, Post } from '@nestjs/common';
import { StandardValidationPipe } from '@mag123c/nestjs-stdschema';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().optional(),
});

@Controller('users')
export class UsersController {
  @Post()
  create(
    @Body(new StandardValidationPipe(CreateUserSchema))
    body: z.infer<typeof CreateUserSchema>,
  ) {
    return body;
  }
}

With DTO Class

import { createStandardDto, StandardValidationPipe } from '@mag123c/nestjs-stdschema';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

// Create a DTO class with automatic type inference
class CreateUserDto extends createStandardDto(CreateUserSchema) {}

@Controller('users')
export class UsersController {
  @Post()
  create(
    @Body(new StandardValidationPipe(CreateUserDto.schema))
    body: CreateUserDto,
  ) {
    // body is fully typed as { name: string; email: string }
    return body;
  }
}

Global Pipe

import { StandardValidationPipe } from '@mag123c/nestjs-stdschema';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global pipe requires @Schema decorator or createStandardDto
  app.useGlobalPipes(new StandardValidationPipe());

  await app.listen(3000);
}

Important: Global pipe relies on TypeScript's design:paramtypes metadata to detect DTO classes. See Requirements for Global Pipe section.

With Valibot

import { StandardValidationPipe } from '@mag123c/nestjs-stdschema';
import * as v from 'valibot';

const CreateUserSchema = v.object({
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
});

@Post()
create(
  @Body(new StandardValidationPipe(CreateUserSchema))
  body: v.InferOutput<typeof CreateUserSchema>,
) {
  return body;
}

Response Serialization

Strip sensitive fields from responses using StandardSerializerInterceptor:

import {
  StandardSerializerInterceptor,
  ResponseSchema,
  createStandardDto,
} from '@mag123c/nestjs-stdschema';
import { z } from 'zod';

const UserResponseSchema = z.object({
  id: z.string(),
  name: z.string(),
  // email and password are excluded from schema
});

class UserResponseDto extends createStandardDto(UserResponseSchema) {}

@Controller('users')
@UseInterceptors(StandardSerializerInterceptor)
export class UsersController {
  @Get(':id')
  @ResponseSchema(UserResponseDto)
  findOne(@Param('id') id: string) {
    // Even if this returns { id, name, email, password },
    // only { id, name } will be sent to the client
    return this.userService.findOne(id);
  }

  @Get()
  @ResponseSchema([UserResponseDto]) // Array response
  findAll() {
    return this.userService.findAll();
  }
}

Global Interceptor

import { Reflector } from '@nestjs/core';
import { StandardSerializerInterceptor } from '@mag123c/nestjs-stdschema';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(
    new StandardSerializerInterceptor(app.get(Reflector))
  );

  await app.listen(3000);
}

Note: The serializer strips extra fields by leveraging the validator's default behavior. Both Zod and Valibot strip unknown keys by default. If your validator preserves extra keys, use its strict/strip mode explicitly.

API Reference

StandardValidationPipe

new StandardValidationPipe(schema?, options?)

Options:

Option Type Default Description
errorHttpStatusCode HttpStatus 400 HTTP status code for validation errors
exceptionFactory (issues) => any - Custom exception factory
validateCustomDecorators boolean false Validate custom decorator parameters
expectedType Type<any> - Override metatype for validation

createStandardDto

function createStandardDto<T extends StandardSchemaV1>(
  schema: T,
  options?: { openapi?: OpenAPIMetadata }
): StandardDtoClass<T>;

Creates a DTO class from a schema with:

  • Static schema property
  • Automatic type inference
  • OpenAPI metadata generation

Decorators

Decorator Description
@Schema(schema) Attach schema to existing class
@ResponseSchema(dto) Define response schema for serialization
@ResponseSchema([dto]) Define array response schema

Utilities

Function Description
getSchema(target) Get schema from DTO class
schemaToOpenAPI(schema, metadata?) Convert schema to OpenAPI format

Type Utilities

import { InferInput, InferOutput } from '@mag123c/nestjs-stdschema';

type Input = InferInput<typeof MySchema>;   // Input type
type Output = InferOutput<typeof MySchema>; // Output type

Error Response Format

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    {
      "path": ["email"],
      "message": "Invalid email"
    },
    {
      "path": ["age"],
      "message": "Expected number, received string"
    }
  ]
}

Custom Error Format

new StandardValidationPipe(schema, {
  errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
  exceptionFactory: (issues) => {
    return new UnprocessableEntityException({
      code: 'VALIDATION_ERROR',
      errors: issues.map(issue => ({
        field: issue.path?.join('.') ?? 'root',
        message: issue.message,
      })),
    });
  },
});

OpenAPI Integration

DTOs created with createStandardDto automatically work with @nestjs/swagger:

import { createStandardDto } from '@mag123c/nestjs-stdschema';
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

class UserDto extends createStandardDto(UserSchema) {}

OpenAPI schema generation:

  • Zod v4+: Automatically generates OpenAPI schema via native toJSONSchema()
  • Zod v3.x / Other validators: Provide manual metadata
// For validators without native toJSONSchema (Zod v3.x, Valibot, etc.)
class UserDto extends createStandardDto(UserSchema, {
  openapi: {
    name: { type: 'string', example: 'John' },
    email: { type: 'string', format: 'email' },
  },
}) {}

Supported Validators

Any validator implementing the standard-schema specification:

Validator Version Status
Zod ^3.24 / ^4.0 Tested
Valibot ^1.0.0 Tested
ArkType ^2.0.0 Compatible*
TypeBox ^0.32.0 Compatible*
And more... See full list

*Compatible: Implements standard-schema spec but not tested in this package. PRs welcome!

Requirements

  • Node.js >= 18
  • NestJS >= 10.0.0
  • TypeScript >= 5.0

Requirements for Global Pipe

When using StandardValidationPipe as a global pipe (without explicitly passing a schema), it relies on TypeScript's design:paramtypes metadata to detect the DTO class and its schema. This is the same mechanism used by NestJS's built-in ValidationPipe.

Required tsconfig.json settings:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

Build tool compatibility:

Build Tool Support Configuration
tsc Supported Default with above tsconfig
SWC Supported Requires decoratorMetadata: true in .swcrc
esbuild Not supported Does not emit decorator metadata
Vite / Vitest Not supported Uses esbuild internally

SWC configuration (.swcrc):

{
  "jsc": {
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    }
  }
}

If your build tool doesn't support decorator metadata, use explicit schema passing instead:

// Instead of relying on global pipe detection:
@Body() dto: CreateUserDto

// Explicitly pass the schema:
@Body(new StandardValidationPipe(CreateUserSchema)) dto: CreateUserDto

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

About

NestJS validation pipe for Zod, Valibot, ArkType and 20+ validators - no vendor lock-in

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •