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

bookish-potato-dto

v2.0.1-rc

Published

A TypeScript DTO (Data Transfer Object) parsing and validation library. Define a schema once — get runtime validation and a fully inferred TypeScript type for free.

Readme

bookish-potato-dto

A TypeScript DTO (Data Transfer Object) parsing and validation library. Define a schema once — get runtime validation and a fully inferred TypeScript type for free.

Table of Contents


Installation

npm install bookish-potato-dto

No tsconfig.json flags required. Works in Node.js, Bun, Deno, and any ESM environment.


Quick Start

import { defineDto, field, InferDto, parseObject } from 'bookish-potato-dto';

const PersonDto = defineDto({
  name:     field.string(),
  age:      field.integer({ strictDataTypes: true }),
  height:   field.number(),
  weight:   field.number({ defaultValue: 70 }),
  eyeColor: field.string({ isOptional: true }),
  active:   field.boolean(),
});

// Derive the TypeScript type — no duplication
type PersonDto = InferDto<typeof PersonDto>;

const person = parseObject(PersonDto, {
  name: 'John Doe',
  age: 30,
  height: 180.5,
  active: true,
});

// person.name     === 'John Doe'
// person.weight   === 70         (defaultValue applied)
// person.eyeColor === undefined  (optional, not provided)

Field builders

| Builder | Description | |---|---| | field.string(opts?) | String field | | field.number(opts?) | Floating-point number | | field.integer(opts?) | Integer | | field.boolean(opts?) | Boolean | | field.enum(E, opts?) | Enum value | | field.date(opts?) | Date instance or ISO string | | field.regex(re, opts?) | String validated against a regex | | field.array(type, opts?) | Array of 'string', 'number', or 'boolean' | | field.arrayDto(Dto, opts?) | Array of nested DTOs | | field.dto(Dto, opts?) | Single nested DTO | | field.custom(opts) | Custom parser instance |


All field options

Common options (all fields)

| Option | Type | Description | |---|---|---| | isOptional | boolean | Field may be absent. TypeScript type becomes T \| undefined. | | isNullable | boolean | Field may be null. TypeScript type becomes T \| null. | | defaultValue | T \| null | Fallback value when field is absent. | | useDefaultValueOnParseError | boolean | Use defaultValue instead of throwing on bad input. | | mapFrom | string | Read from a differently-named key in the raw input. | | parsingErrorMessage | (key, value, error) => string | Custom error message function. | | openApi | OpenApiSchema | Manual overrides or additional metadata for OpenAPI generation. |

String options (field.string)

| Option | Type | Description | |---|---|---| | minLength | number | Minimum string length. | | maxLength | number | Maximum string length. |

Number and integer options (field.number, field.integer)

| Option | Type | Description | |---|---|---| | strictDataTypes | boolean | Disable string→number coercion. | | minValue | number | Minimum value. | | maxValue | number | Maximum value. |

Boolean options (field.boolean)

| Option | Type | Description | |---|---|---| | strictDataTypes | boolean | Disable "true"/"false" string coercion. |

Array options (field.array)

| Option | Type | Description | |---|---|---| | minLength | number | Minimum array length. | | maxLength | number | Maximum array length. | | strictDataTypes | boolean | Disable coercion for items. | | stringsLength | { minLength?, maxLength? } | Per-item length constraint (string arrays). | | numbersRange | { minValue?, maxValue? } | Per-item range constraint (number arrays). |

Array of DTO options (field.arrayDto)

| Option | Type | Description | |---|---|---| | minLength | number | Minimum array length. | | maxLength | number | Maximum array length. |


Composing and extending DTOs

Schemas are plain objects. Use the spread operator to compose them:

const PersonDto = defineDto({
  name: field.string(),
  age:  field.integer(),
});

// Extend with new fields
const EmployeeDto = defineDto({
  ...PersonDto.fields,
  position: field.string(),
});

// Three-way composition
const TimestampedDto = defineDto({ createdAt: field.date() });
const AuditedEmployeeDto = defineDto({
  ...EmployeeDto.fields,
  ...TimestampedDto.fields,
});

Nested DTOs

const AddressDto = defineDto({
  street: field.string(),
  city:   field.string(),
});

const PersonDto = defineDto({
  name:      field.string(),
  address:   field.dto(AddressDto),          // single nested DTO
  addresses: field.arrayDto(AddressDto),     // array of nested DTOs
});

Enum fields

enum Role { Admin = 'admin', User = 'user', Guest = 'guest' }

const UserDto = defineDto({
  name: field.string(),
  role: field.enum(Role),
});

Custom parsers

class CsvParser {
  parse(value: unknown): string[] {
    if (typeof value !== 'string') throw new Error('not a string');
    return value.split(',').filter(Boolean);
  }
}

const ConfigDto = defineDto({
  port:     field.integer({ mapFrom: 'PORT', defaultValue: 3000 }),
  origins:  field.custom({ mapFrom: 'ALLOWED_ORIGINS', parser: new CsvParser() }),
});

OpenAPI Schema Generation

bookish-potato-dto can automatically generate OpenAPI 3.0/3.1 compatible schemas from your DTO definitions. It infers types, constraints (like minLength, minimum), nullability, and default values.

import { defineDto, field, generateOpenApi } from 'bookish-potato-dto';

const UserDto = defineDto({
  id:    field.string({ openApi: { format: 'uuid' } }),
  email: field.string({ openApi: { format: 'email', description: 'User contact email' } }),
  age:   field.integer({ minValue: 18 }),
});

const { schema, refs } = generateOpenApi(UserDto, {
  // Optional: provide meaningful names for $ref resolution
  nameResolver: (dto) => dto === UserDto ? 'User' : dto._uuid
});

// schema: { $ref: '#/components/schemas/User' }
// refs.User: {
//   type: 'object',
//   properties: {
//     id: { type: 'string', format: 'uuid' },
//     email: { type: 'string', format: 'email', description: 'User contact email' },
//     age: { type: 'integer', minimum: 18 }
//   },
//   required: ['id', 'email', 'age']
// }

Manual Overrides

Use the openApi option on any field to add descriptions, examples, or override the automatically inferred schema.

field.string({
  minLength: 5,
  openApi: {
    description: 'A unique username',
    example: 'john_doe',
    maxLength: 20
  }
})

Type inference with InferDto

InferDto<T> produces the full TypeScript type from a DtoDefinition. Optional fields (isOptional: true) become T | undefined. Nullable fields (isNullable: true) become T | null.

const PersonDto = defineDto({
  name:     field.string(),
  age:      field.integer(),
  nickname: field.string({ isOptional: true }),
  score:    field.number({ isNullable: true }),
});

type PersonDto = InferDto<typeof PersonDto>;
// Equivalent to:
// {
//   name:      string;
//   age:       number;
//   nickname?: string;
//   score:     number | null;
// }

Use cases

REST API body parsing

enum Roles { Admin = 'admin', User = 'user' }

const RoleDto = defineDto({
  name: field.string(),
  role: field.enum(Roles),
});

const UserDto = defineDto({
  name:  field.string(),
  email: field.string(),
  age:   field.integer(),
  role:  field.dto(RoleDto),
});

// In your request handler:
const user = parseObject(UserDto, req.body);

Environment / config parsing

enum LogLevel { Info = 'info', Debug = 'debug', Error = 'error' }

const ConfigDto = defineDto({
  port:           field.integer({ mapFrom: 'PORT', defaultValue: 3000 }),
  logLevel:       field.enum(LogLevel, { mapFrom: 'LOG_LEVEL', defaultValue: LogLevel.Info }),
  dbSecret:       field.string({ mapFrom: 'DB_SECRET' }),
  allowedOrigins: field.custom({ mapFrom: 'ALLOWED_ORIGINS', parser: new CsvParser() }),
});

export const config = parseObject(ConfigDto, process.env);

Data transformation

const PersonDto = defineDto({
  id:     field.string({ mapFrom: 'uuid' }),
  name:   field.string(),
  status: field.string({ defaultValue: 'active' }),
});

const person = parseObject(PersonDto, { uuid: '123', name: 'Alice' });
// person.id === '123'
// person.status === 'active'

Feature Requests, Bugs Reports, and Contributions

Please use the GitHub Issues repository to report bugs, request features, or ask questions.