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.
Maintainers
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
- Quick Start
- Field builders
- All field options
- Composing and extending DTOs
- Nested DTOs
- Enum fields
- Custom parsers
- OpenAPI Schema Generation
- Type inference with InferDto
- Use cases
- Feature Requests & Bug Reports
Installation
npm install bookish-potato-dtoNo 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.
