UNPKG

bookish-potato-dto

Version:

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.

343 lines (261 loc) 9.28 kB
# 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](#installation) - [Quick Start](#quick-start) - [Field builders](#field-builders) - [All field options](#all-field-options) - [Composing and extending DTOs](#composing-and-extending-dtos) - [Nested DTOs](#nested-dtos) - [Enum fields](#enum-fields) - [Custom parsers](#custom-parsers) - [OpenAPI Schema Generation](#openapi-schema-generation) - [Type inference with InferDto](#type-inference-with-inferdto) - [Use cases](#use-cases) - [REST API body parsing](#rest-api-body-parsing) - [Environment / config parsing](#environment--config-parsing) - [Data transformation](#data-transformation) - [Feature Requests & Bug Reports](#feature-requests-bugs-reports-and-contributions) --- ## Installation ```bash npm install bookish-potato-dto ``` No `tsconfig.json` flags required. Works in Node.js, Bun, Deno, and any ESM environment. --- ## Quick Start ```typescript 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: ```typescript 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 ```typescript 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 ```typescript enum Role { Admin = 'admin', User = 'user', Guest = 'guest' } const UserDto = defineDto({ name: field.string(), role: field.enum(Role), }); ``` --- ## Custom parsers ```typescript 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. ```typescript 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. ```typescript 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`. ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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](https://github.com/andrei-trukhin/bookish-potato-dto-issues) repository to report bugs, request features, or ask questions.