npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nestjs-io-ts

v1.2.0

Published

io-ts runtime type validation for NestJS with built-in branded types and OpenAPI integration

Readme

Publish Package

nestjs-io-ts

A utility library that integrates io-ts with the NestJS framework for runtime type validation.

Features

  • Validation Pipe: Use io-ts for runtime type validation in NestJS controllers
  • DTO Creation: Easily create DTOs using io-ts codecs with full TypeScript inference
  • Structured Error Responses: Detailed, field-level validation error messages with error codes and suggestions
  • Custom Error Messages: Customize validation messages with withMessage API
  • Codec Combinators: optional, nullable, withDefault, crossValidate, transform for common patterns
  • Cross-field Validation: Validate relationships between fields (password confirmation, date ranges)
  • Value Transformation: Transform input values after validation (trim, lowercase, parse dates)
  • DTO Utilities: Create Partial, Pick, Omit DTOs for flexible validation scenarios
  • Query String Coercion: Automatically convert query strings to correct types
  • OpenAPI Integration: Automatic OpenAPI schema generation from io-ts types
  • Built-in Types: Pre-built branded types for common validations (Email, UUID, URL, Phone, etc.)

Installation

npm install nestjs-io-ts io-ts fp-ts

Quick Start

1. Create a DTO from an io-ts codec

import * as t from 'io-ts';
import { createIoTsDto, Email } from 'nestjs-io-ts';

const CreateUserCodec = t.type({
  name: t.string,
  email: Email,
  age: t.union([t.number, t.undefined]),
});

export class CreateUserDto extends createIoTsDto(CreateUserCodec) {}

2. Use the DTO in a Controller

import { Body, Controller, Post, UsePipes } from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
import { IoTsValidationPipe } from 'nestjs-io-ts';

@Controller('users')
export class UsersController {
  @Post()
  @UsePipes(IoTsValidationPipe)
  createUser(@Body() dto: CreateUserDto) {
    // dto is fully typed and validated
    return dto;
  }
}

3. Global Pipe (Recommended)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { IoTsValidationPipe } from 'nestjs-io-ts';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new IoTsValidationPipe());
  await app.listen(3000);
}
bootstrap();

Validation Pipe Options

The IoTsValidationPipe accepts optional configuration:

import { IoTsValidationPipe } from 'nestjs-io-ts';

// With options
new IoTsValidationPipe({
  // Allow non-IoTsDto types to pass through without validation
  allowPassthrough: true,

  // Only validate specific argument types (default: ['body', 'query', 'param'])
  validateTypes: ['body'],

  // Automatically coerce query/param strings to correct types (default: false)
  coerceQueryStrings: true,
});

// With explicit codec
new IoTsValidationPipe(MyCodec);

// With codec and options
new IoTsValidationPipe(MyCodec, { validateTypes: ['body'] });

Query String Coercion

When coerceQueryStrings is enabled, string values from query parameters are automatically converted:

  • "true" / "false"boolean
  • "123" / "3.14"number
  • "null"null
  • "undefined"undefined
// ?page=1&limit=10&active=true becomes { page: 1, limit: 10, active: true }
@Get()
@UsePipes(new IoTsValidationPipe({ coerceQueryStrings: true }))
findAll(@Query() query: PaginationDto) {}

Error Response Format

When validation fails, the pipe throws an IoTsValidationException with structured error details:

{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "Bad Request",
  "errors": [
    {
      "field": "email",
      "message": "Invalid email format",
      "code": "INVALID_EMAIL",
      "expected": "Email",
      "value": "invalid-email",
      "suggestion": "Please provide a valid email address (e.g., [email protected])"
    },
    {
      "field": "age",
      "message": "Expected number but received string",
      "code": "INVALID_TYPE",
      "expected": "number",
      "value": "not-a-number",
      "suggestion": "Please provide a valid number"
    }
  ]
}

Array Error Paths

Errors in array items include the index in bracket notation for precise identification:

{
  "errors": [
    {
      "field": "items[1].price",
      "message": "Expected number but received string",
      "code": "INVALID_TYPE"
    },
    {
      "field": "tags[2]",
      "message": "Expected string but received number",
      "code": "INVALID_TYPE"
    }
  ]
}

Custom Error Messages

Use withMessage to customize validation messages:

import { withMessage, Email } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  email: withMessage(Email, {
    invalid: 'Please enter a valid email address',
    required: 'Email is required',
  }),
  age: withMessage(t.number, {
    invalid: (value) => `Age must be a number, got ${typeof value}`,
  }),
});

Built-in Extended Types

String Types

| Type | Description | Example | | ----------------- | ------------------------------------------ | ----------------------------------------- | | Email | Valid email address (RFC 5322 subset) | [email protected] | | UUID | UUID v1-5 format | 550e8400-e29b-41d4-a716-446655440000 | | URL | HTTP/HTTPS URL | https://example.com/path | | Phone | Phone number (E.164 compatible) | +1-234-567-8900 | | DateString | ISO 8601 date | 2024-01-15 | | DateTimeString | ISO 8601 datetime | 2024-01-15T10:30:00Z | | IPv4 | IPv4 address | 192.168.0.1 | | IPv6 | IPv6 address | 2001:db8::ff00:42:8329 | | IP | IPv4 or IPv6 address | Either format | | NonEmptyString | Non-empty string | hello | | TrimmedString | String without leading/trailing whitespace | hello world | | LowercaseString | Lowercase only string | hello | | UppercaseString | Uppercase only string | HELLO | | HexColor | Hex color code | #ff0000 or #f00 | | Slug | URL-safe slug | my-blog-post-123 | | Base64 | Base64 encoded string | SGVsbG8gV29ybGQ= | | JWT | JSON Web Token format | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |

Number Types

| Type | Description | Example | | ------------------- | --------------------------- | --------- | | Integer | Integer (no decimals) | 42 | | PositiveNumber | Number > 0 | 3.14 | | NonNegativeNumber | Number >= 0 | 0, 42 | | PositiveInteger | Integer > 0 | 1, 42 | | Port | Valid port number (0-65535) | 8080 | | Percentage | Number between 0-100 | 75.5 |

Usage Example

import * as t from 'io-ts';
import {
  createIoTsDto,
  Email,
  UUID,
  Phone,
  DateString,
  Port,
  NonEmptyString,
} from 'nestjs-io-ts';

const UserProfileCodec = t.type({
  id: UUID,
  email: Email,
  phone: t.union([Phone, t.undefined]),
  displayName: NonEmptyString,
  birthDate: t.union([DateString, t.undefined]),
});

export class UserProfileDto extends createIoTsDto(UserProfileCodec) {}

Codec Combinators

Utility functions for common validation patterns.

optional

Creates a codec that accepts undefined in addition to the base type:

import { optional } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  nickname: optional(t.string), // string | undefined
});

// Valid inputs:
// { name: "John" }
// { name: "John", nickname: undefined }
// { name: "John", nickname: "Johnny" }

nullable

Creates a codec that accepts null in addition to the base type (useful for database nullable fields):

import { nullable } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  deletedAt: nullable(t.string), // string | null
});

// Valid inputs:
// { name: "John", deletedAt: null }
// { name: "John", deletedAt: "2024-01-01" }

withDefault

Creates a codec that provides a default value when input is undefined:

import { withDefault } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  role: withDefault(t.string, 'user'),
  isActive: withDefault(t.boolean, true),
});

// Input: { name: "John" }
// Output: { name: "John", role: "user", isActive: true }

// Input: { name: "John", role: "admin" }
// Output: { name: "John", role: "admin", isActive: true }

crossValidate

Performs cross-field validation after the base codec validates. Useful for password confirmation, date ranges, etc.:

import { crossValidate } from 'nestjs-io-ts';
import * as t from 'io-ts';

// Password confirmation
const PasswordResetCodec = crossValidate(
  t.type({
    password: t.string,
    confirmPassword: t.string,
  }),
  (data) => {
    if (data.password !== data.confirmPassword) {
      return [{ field: 'confirmPassword', message: 'Passwords do not match' }];
    }
    return [];
  },
);

// Date range validation
const DateRangeCodec = crossValidate(
  t.type({
    startDate: t.string,
    endDate: t.string,
  }),
  (data) => {
    if (new Date(data.startDate) > new Date(data.endDate)) {
      return [
        { field: 'endDate', message: 'End date must be after start date' },
      ];
    }
    return [];
  },
);

transform

Transforms the decoded value after successful validation. Useful for normalizing input:

import { transform } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  // Trim whitespace from input
  name: transform(t.string, (s) => s.trim()),

  // Normalize email to lowercase
  email: transform(t.string, (s) => s.toLowerCase().trim()),

  // Parse date string to Date object
  birthDate: transform(t.string, (s) => new Date(s)),
});

// Input: { name: "  John  ", email: "  [email protected]  ", birthDate: "2000-01-15" }
// Output: { name: "John", email: "[email protected]", birthDate: Date object }

Combining Combinators

You can combine these utilities for complex scenarios:

import { optional, nullable, withDefault, transform } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  email: Email,
  nickname: optional(t.string), // can be omitted
  deletedAt: nullable(DateTimeString), // explicit null for soft delete
  role: withDefault(t.string, 'user'), // defaults to 'user'
  displayName: transform(t.string, (s) => s.trim()), // trimmed input
});

DTO Utilities

Partial DTO

Create DTOs where all fields are optional (useful for PATCH endpoints):

import { createPartialDto } from 'nestjs-io-ts';

const UserCodec = t.type({
  name: t.string,
  email: Email,
  age: t.number,
});

// All fields become optional
class UpdateUserDto extends createPartialDto(UserCodec) {}

@Patch(':id')
updateUser(@Body() dto: UpdateUserDto) {
  // dto.name, dto.email, dto.age are all optional
}

Pick DTO

Create DTOs with only specific fields:

import { createPickDto } from 'nestjs-io-ts';

// Only include email and name
class UserContactDto extends createPickDto(UserCodec, ['email', 'name']) {}

Omit DTO

Create DTOs excluding specific fields:

import { createOmitDto } from 'nestjs-io-ts';

// Exclude id and createdAt for creation
class CreateUserDto extends createOmitDto(UserCodec, ['id', 'createdAt']) {}

Intersection DTO

Combine required and optional fields:

import { createIntersectionDto } from 'nestjs-io-ts';

const RequiredFields = t.type({
  name: t.string,
  email: Email,
});

const OptionalFields = t.partial({
  age: t.number,
  bio: t.string,
});

class CreateUserDto extends createIntersectionDto(
  RequiredFields,
  OptionalFields,
) {}
// { name: string, email: Email, age?: number, bio?: string }

OpenAPI Integration

The library provides automatic OpenAPI schema generation from io-ts types:

import { ioTsToOpenAPI } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  email: Email,
  age: t.number,
});

const schema = ioTsToOpenAPI(UserCodec);
// Returns:
// {
//   type: 'object',
//   properties: {
//     name: { type: 'string' },
//     email: { type: 'string', format: 'email' },
//     age: { type: 'number' }
//   },
//   required: ['name', 'email', 'age']
// }

Branded types are automatically mapped to OpenAPI formats:

| io-ts Type | OpenAPI Format | | ---------------- | ------------------------------------------- | | Email | string with format: 'email' | | UUID | string with format: 'uuid' | | URL | string with format: 'uri' | | DateString | string with format: 'date' | | DateTimeString | string with format: 'date-time' | | IPv4 | string with format: 'ipv4' | | IPv6 | string with format: 'ipv6' | | Integer | integer | | Port | integer with minimum: 0, maximum: 65535 |

Manual Validation

You can also validate data manually without using the pipe:

import { decodeAndThrow, IoTsValidationException } from 'nestjs-io-ts';
import * as t from 'io-ts';

const UserCodec = t.type({
  name: t.string,
  age: t.number,
});

try {
  const user = decodeAndThrow({ name: 'John', age: 30 }, UserCodec);
  console.log(user); // { name: 'John', age: 30 }
} catch (error) {
  if (error instanceof IoTsValidationException) {
    console.log(error.getErrors());
  }
}

Or use the DTO's static create method:

import { createIoTsDto } from 'nestjs-io-ts';

const UserDto = createIoTsDto(UserCodec);

try {
  const user = UserDto.create({ name: 'John', age: 30 });
} catch (error) {
  // Handle validation error
}

API Reference

createIoTsDto(codec)

Creates a DTO class from an io-ts codec.

  • Parameters: codec - An io-ts type codec
  • Returns: A class that can be used as a DTO with validation

IoTsValidationPipe

NestJS validation pipe for io-ts validation.

  • Constructor: new IoTsValidationPipe(codecOrOptions?, options?)
  • Options:
    • allowPassthrough: Allow non-IoTsDto types to pass through (default: false)
    • validateTypes: Argument types to validate (default: ['body', 'query', 'param'])
    • coerceQueryStrings: Auto-convert query strings to correct types (default: false)

IoTsValidationException

Exception thrown when validation fails.

  • Methods:
    • getErrors(): Returns array of ValidationError objects
    • getResponse(): Returns the full error response object

decodeAndThrow(value, codecOrDto)

Validates a value and throws if validation fails.

  • Parameters:
    • value - The value to validate
    • codecOrDto - An io-ts codec or IoTsDto class
  • Returns: The decoded value
  • Throws: IoTsValidationException if validation fails

ioTsToOpenAPI(codec)

Converts an io-ts codec to an OpenAPI schema object.

  • Parameters: codec - An io-ts type codec
  • Returns: OpenAPI SchemaObject

isIoTsDto(obj)

Type guard to check if an object is an IoTsDto class.

  • Parameters: obj - The object to check
  • Returns: boolean

withMessage(codec, messages)

Wraps an io-ts codec with custom error messages.

  • Parameters:
    • codec - An io-ts codec to wrap
    • messages - Custom error messages configuration or message function
  • Returns: A new codec with custom error messages

createPartialDto(codec)

Creates a DTO where all fields are optional.

  • Parameters: codec - An io-ts t.type codec
  • Returns: A new IoTsDto with all fields optional

createPickDto(codec, keys)

Creates a DTO with only specified fields.

  • Parameters:
    • codec - An io-ts t.type codec
    • keys - Array of keys to include
  • Returns: A new IoTsDto with only the specified fields

createOmitDto(codec, keys)

Creates a DTO excluding specified fields.

  • Parameters:
    • codec - An io-ts t.type codec
    • keys - Array of keys to exclude
  • Returns: A new IoTsDto without the specified fields

createIntersectionDto(codecA, codecB)

Combines two codecs into one DTO.

  • Parameters:
    • codecA - First io-ts codec
    • codecB - Second io-ts codec
  • Returns: A new IoTsDto with combined fields

optional(codec)

Creates a codec that accepts undefined in addition to the base type.

  • Parameters: codec - An io-ts codec
  • Returns: A union codec of [codec, t.undefined]

nullable(codec)

Creates a codec that accepts null in addition to the base type.

  • Parameters: codec - An io-ts codec
  • Returns: A union codec of [codec, t.null]

withDefault(codec, defaultValue)

Creates a codec that provides a default value when input is undefined.

  • Parameters:
    • codec - An io-ts codec
    • defaultValue - The default value to use when input is undefined
  • Returns: A new codec that returns the default value for undefined inputs

crossValidate(codec, validator)

Creates a codec that performs cross-field validation after the base codec validates.

  • Parameters:
    • codec - An io-ts codec
    • validator - Function that receives decoded data and returns an array of { field, message } errors
  • Returns: A new codec that includes cross-field validation

transform(codec, transformer)

Creates a codec that transforms the decoded value after successful validation.

  • Parameters:
    • codec - An io-ts codec
    • transformer - Function to transform the decoded value
  • Returns: A new codec that transforms values after decoding

License

MIT