# nestjs-typebox

This library provides helper utilities for writing and validating NestJS APIs using [TypeBox](https://github.com/sinclairzx81/typebox) as
an alternative to class-validator/class-transformer. Can be configured to patch @nestjs/swagger allowing OpenAPI generation to continue working.
Supports property defaults, basic type coercion, transforms, stripping unknown properties, and custom error messages. See typebox docs for more info.

## Installation

```sh
npm i nestjs-typebox @sinclair/typebox
```

> **Important:** Note that `nestjs-typebox` is an alternative to the class-validator DTO approach detailed in the NestJS docs, and is
> meant to fully replace it and all of the built-in validation/parsing pipes. Make sure you remove any global validation/parsing pipes
> before installing this library and avoid using any local validation/parsing pipe decorators in combination with this library's decorators.

## Usage

### 1. Create TypeBox schema

> The example below demonstrates a discriminated union type,
> which cannot be achieved using class-based introspection approaches like that of class-validator.

```ts
import { Type } from '@sinclair/typebox';

export const PetSchemaBase = Type.Object({
    id: Type.Number(),
    name: Type.String({
        description: "The pet's name",
        examples: ['Figaro'],
    }),
    microchip: Type.String(){
        minLength: 10,
        description: 'Secret microchip number. Not sent to client',
        errorMessage: '"microchip" is required and must be at least 10 characters.'
    },
});

export const CatSchema = Type.Composite([
    PetSchemaBase,
    Type.Object({
        type: Type.Literal('cat'),
        breed: Type.Union([Type.Literal('shorthair'), Type.Literal('persian'), Type.Literal('siamese')]),
    }),
]);

export const DogSchema = Type.Composite([
    PetSchemaBase,
    Type.Object({
        type: Type.Literal('dog'),
        breed: Type.Union([Type.Literal('shiba-inu'), Type.Literal('poodle'), Type.Literal('dachshund')]),
    }),
]);

export const PetSchema = Type.Union([CatSchema, DogSchema]);
export type Pet = Static<typeof PetSchema>;
```

### 2. Decorate controller methods

> The example below shows two different decorators and their usage, calling out default configuration.
> Schemas have all been defined inline for brevity, but could just as easily be defined elsewhere
> and reused. The primary benefit of using @HttpEndpoint over @Validator is the additional validation
> enforcing path parameters to be properly defined as request "param" validators. Otherwise, it simply
> passes through options specified in `validate` to the underlying @Validator decorator.

```ts
import { Type } from '@sinclair/typebox';
import { Validate, HttpEndpoint } from 'nestjs-typebox';

@Controller('pets')
export class PetController {
    constructor(private readonly petService: PetService) {}

    @Get()
    @Validate({
        response: { schema: Type.Array(Type.Omit(PetSchema, ['microchip'])), stripUnknownProps: true },
    })
    async getPets() {
        return this.petService.getPets();
    }

    @Get(':id')
    @Validate({
        // stripUnknownProps is true by default for response validators
        // so this shorthand is equivalent
        response: Type.Omit(PetSchema, ['microchip']),
        request: [
            // coerceTypes is true by default for "param" and "query" request validators
            { name: 'id', type: 'param', schema: Type.Number(), coerceTypes: true },
        ],
    })
    // no need to use @Param() decorator here since the @Validate() decorator will
    // automatically attach a pipe to populate and convert the paramater value
    async getPet(id: number) {
        return this.petService.getPet(id);
    }

    @Post()
    @Validate({
        response: Type.Omit(PetSchema, ['microchip']),
        request: [
            // if "name" not provided, method name will be used
            { type: 'body', schema: Type.Omit(PetSchema, 'id') },
        ],
    })
    async createPet(data: Omit<Pet, 'id'>) {
        return this.petService.createPet(data);
    }

    @HttpEndpoint({
        method: 'PATCH',
        path: ':id',
        validate: {
            response: Type.Omit(PetSchema, ['microchip']),
            request: [
                { name: 'id', type: 'param', schema: Type.Number() },
                { type: 'body', schema: Type.Partial(Type.Omit(PetSchema, ['id'])) },
            ],
        },
    })
    // the order of the controller method parameters must correspond to the order/types of
    // "request" validators, including "required" configuration. Additionally nestjs-typebox will
    // throw at bootup if parameters defined in the "request" validator config don't correspond
    // with the parameters defined in the "path" configuration
    async updatePet(id: number, data: Partial<Omit<Pet, 'id'>>) {
        return this.petService.updatePet(id, data);
    }

    @HttpEndpoint({
        method: 'DELETE',
        path: ':id',
        validate: {
            response: Type.Omit(PetSchema, ['microchip']),
            request: [{ name: 'id', type: 'param', schema: Type.Number() }],
        },
    })
    async deletePet(id: number) {
        return this.petService.deletePet(id);
    }
}
```

### 3. Optionally configure

Calling configure allows for the patching of the swagger plugin, custom
string formats (email, url, date, time, date-time, uuid), and support for `errorMessage` overrides
within schema options.

```ts
// main.ts

import { Reflector } from '@nestjs/core';
import { configureNestJsTypebox } from 'nestjs-typebox';

configureNestJsTypebox({
    patchSwagger: true,
    setFormats: true,
});

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

    await app.listen(3000);
    console.log(`Application is running on: ${await app.getUrl()}`);
}

bootstrap();
```

### Credits

Swagger patch derived from https://github.com/risenforces/nestjs-zod

### Todo

-   Validate observable support
-   utility to create typebox schemas with CRUD defaults (i.e. SchemaName['response'], SchemaName['update'])
-   include method name in decorator errors
-   support validating entire query object? (instead of individual values)
-   check controller metadata so resolved path can include params specified at the controller level
