nestjs-joi
Version:
Easy to use JoiPipe as an interface between joi and NestJS with optional decorator-based schema construction.
428 lines (314 loc) • 19.1 kB
Markdown
## `nestjs-joi`
<a href="https://www.npmjs.com/package/nestjs-joi" target="_blank"><img src="https://img.shields.io/npm/v/nestjs-joi.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/package/nestjs-joi" target="_blank"><img src="https://img.shields.io/npm/l/nestjs-joi.svg" alt="Package License" /></a>
[](https://coveralls.io/github/whefter/nestjs-joi)


Easy to use `JoiPipe` as an interface between `joi` and NestJS with optional decorator-based schema construction. Based on [`joi-class-decorators`](https://github.com/whefter/joi-class-decorators).
- [Installation](#installation)
- [Peer dependencies](#peer-dependencies)
- [Usage](#usage)
- [A note on `/graphql`](#a-note-on-nestjsgraphql)
- [Reference](#reference)
- [Validation groups](#validation-groups)
- [Built-in groups: `DEFAULT`, `CREATE`, `UPDATE`](#built-in-groups-default-create-update)
- [`JoiPipe`](#joipipe)
- [`new JoiPipe(pipeOpts?)`](#new-joipipepipeopts)
- [`new JoiPipe(type, pipeOpts?)`](#new-joipipetype-pipeopts)
- [`new JoiPipe(joiSchema, pipeOpts?)`](#new-joipipejoischema-pipeopts)
- [Pipe options (`pipeOpts`)](#pipe-options-pipeopts)
- [Injection-enabled mode: `JoiPipe` (`(JoiPipe)`, `(JoiPipe)`, ...)](#injection-enabled-mode-joipipe-queryjoipipe-paramjoipipe-)
- [Defining `pipeOpts` in injection-enabled mode](#defining-pipeopts-in-injection-enabled-mode)
- [Error handling and custom schema errors](#error-handling-and-custom-schema-errors)
- [`JoiPipeModule`](#joipipemodule)
- [`JoiPipeValidationException`](#joipipevalidationexception)
- [`()` property decorator](#joischema-property-decorator)
- [`()` class decorator](#joischemaoptions-class-decorator)
- [`(type)` class decorator](#joischemaextendstype-class-decorator)
- [`(callback)` class decorator](#joischemacustomizationcallback-class-decorator)
- [`getClassSchema(typeClass, opts?: { group? })` (alias: `getTypeSchema()`)](#getclassschematypeclass-opts--group--alias-gettypeschema)
# Installation
```bash
npm install --save nestjs-joi
```
## Peer dependencies
```bash
npm install --save /common@^7 @nestjs/core@^7 joi@^17 reflect-metadata@^0.1
```
# Usage
Annotate your type/DTO classes with property schemas and options, then set up your NestJS module to import `JoiPipeModule` to have your controller routes auto-validated everywhere the type/DTO class is used.
The built-in groups `CREATE` and `UPDATE` are available for `POST/PUT` and `PATCH`, respectively.
The `()`, `()`, `()` decorators and the `getTypeSchema()` function are re-exported from the `joi-class-decorators` package.
```typescript
import { JoiPipeModule, JoiSchema, JoiSchemaOptions, CREATE, UPDATE } from 'nestjs-joi';
import * as Joi from 'joi';
({
controllers: [BookController],
imports: [JoiPipeModule],
})
export class AppModule {}
({
allowUnknown: false,
})
export class BookDto {
(Joi.string().required())
([CREATE], Joi.string().required())
([UPDATE], Joi.string().optional())
name!: string;
(Joi.string().required())
([CREATE], Joi.string().required())
([UPDATE], Joi.string().optional())
author!: string;
(Joi.number().optional())
publicationYear?: number;
}
('/books')
export class BookController {
('/')
async createBook(() createData: BookDto) {
// Validated creation data!
return await this.bookService.createBook(createData);
}
('/')
async createBook(() createData: BookDto) {
// Validated create data!
return await this.bookService.createBook(createData);
}
('/')
async createBook(() updateData: BookDto) {
// Validated update data!
return await this.bookService.createBook(createData);
}
}
```
It is possible to use `JoiPipe` on its own, without including it as a global pipe. See below for a more complete documentation.
## A note on `/graphql`
This module can be used with `/graphql`, but with some caveats:
1. passing `new JoiPipe()` to `useGlobalPipes()`, `()`, a pipe defined in `()` etc. works as expected.
2. passing the `JoiPipe` constructor to `useGlobalPipes()`, `()`, `()` etc. does **not** respect the passed HTTP method, meaning that the `CREATE`, `UPDATE` etc. groups will not be used automatically. This limitation is due, to the best of understanding, to Apollo, the GraphQL server used by `/graphql`, which only processed GraphQL queries for if they are sent as `GET` and `POST`.
3. if `JoiPipe` is registered as a global pipe by defining an `APP_PIPE` provider, then **JoiPipe will not be called for GraphQL requests** (see https://github.com/nestjs/graphql/issues/325)
If you want to make sure a validation group is used for a specific resolver mutation, create a new pipe with `new JoiPipe({group: 'yourgroup'})` and pass it to `()` or `()`.
To work around the issue of `OmitType()` etc. breaking the inheritance chain for schema building, see `()` below.
## A note on `/microservice`
This module can be used with `/microservice`, but with some caveats:
1. `JoiPipe` will **not** throw an `RpcException`. It will either throw the usual `BadRequestException` (like for HTTP/GraphQL) or, if you set `usePipeValidationException` to `true`, a `JoiPipeValidationException` (detailed further below). As a result, when you use a `ClientProxy` to invoke a microservice method, NestJS will only return a generic `Internal server error` error object.
- **Why?** There are some cases in which it is now possible to reliably determine if the current context is an RPC context (when the pipe instance is created with `new JoiPipe`). Handling the other cases, but not these, could potentially confuse users (why is this error generic here, but not there?). It is better to create a defined scenario (handle the error)
- **How to handle it**: To obtain and handle the actual exception, you can create an ExceptionFilter for either to handle them (e.g. with `()`) and turn them into an `RpcException`. You can use the provided `JoiPipeValidationRpcExceptionFilter` class or use it as an example. Don't forget to set `usePipeValidationException` to `true`.
# Reference
## Validation groups
Groups can be used to annotate a property (``) or class (``) with different schemas/options for different use cases without having to define a new type.
A straightforward use case for this is a type/DTO that behaves slightly differently in each of the CREATE and UPDATE scenarios. The built-in groups explained below are meant to make interfacing with that use case easier. Have a look at the example in the [Usage](#usage) section.
**For more information, have a look at the** [**validation groups documentation from `joi-class-decorators`**](https://github.com/whefter/joi-class-decorators#validation-groups).
### Built-in groups: `DEFAULT`, `CREATE`, `UPDATE`
Three built-in groups are defined:
- `DEFAULT` is the default "group" assigned under the hood to any schema defined on a property, or any options defined on a class, if a group is not explicitely specified. **This is the same Symbol exported from the `joi-class-decorators` package.**
- `CREATE` is used for validation if `JoiPipe` is used in injection-enabled mode (either through `JoiPipeModule` or `(JoiPipe)` etc.) and the request method is either `POST` or `PUT`
- `PUT` is defined as being capable of completely replacing a resource or creating a new one in case a unique key is not found, which means all properties must be present the same way as for `POST`.
- `UPDATE` works the same way as `CREATE`, but is used if the request method is `PATCH`.
They can be imported in one of two ways, depending on your preference:
```typescript
import { value JoiValidationGroups } from 'nestjs-joi';
import { value DEFAULT, value CREATE, value UPDATE } from 'nestjs-joi';
JoiValidationGroups.CREATE === CREATE; // true
```
## `JoiPipe`
`JoiPipe` can be used either as a global pipe (see below for [`JoiPipeModule`](#joipipemodule)) or for specific requests inside the `()`, `` etc. Request decorators.
When used with the the Request decorators, there are two possibilities:
- pass a configured `JoiPipe` instance
- pass the `JoiPipe` constuctor itself to leverage the injection and built-in group capabilities
When handling a request, the `JoiPipe` instance will be provided by NestJS with the payload and, if present, the `metatype` (`BookDto` in the example below). The `metatype` is used to determine the schema that the payload is validated against, unless `JoiPipe` is instanciated with an explicit type or schema. This is done by evaluating metadata set on the `metatype`'s class properties, if present.
```typescript
('/books')
export class BookController {
('/')
async createBook((JoiPipe) createData: BookDto) {
// Validated creation data!
return await this.bookService.createBook(createData);
}
}
```
### `new JoiPipe(pipeOpts?)`
A `JoiPipe` that will handle payloads based on a schema determined by the passed `metatype`, if present.
If `group` is passed in the `pipeOpts`, only decorators specified for that group or the `DEFAULT` group will be used to construct the schema.
```typescript
('/')
async createBook((new JoiPipe({ group: CREATE })) createData: BookDto) {
// Validated creation data!
return await this.bookService.createBook(createData);
}
```
### `new JoiPipe(type, pipeOpts?)`
A `JoiPipe` that will handle payloads based on the schema constructed from the passed `type`. This pipe will ignore the request `metatype`.
If `group` is passed in the `pipeOpts`, only decorations specified for that group or the `DEFAULT` group will be used to construct the schema.
```typescript
('/')
async createBook((new JoiPipe(BookDto, { group: CREATE })) createData: unknown) {
// Validated creation data!
return await this.bookService.createBook(createData);
}
```
### `new JoiPipe(joiSchema, pipeOpts?)`
A `JoiPipe` that will handle payloads based on the schema passed in the constructor parameters. This pipe will ignore the request `metatype`.
If `group` is passed in the `pipeOpts`, only decorations specified for that group or the `DEFAULT` group will be used to construct the schema.
```typescript
('/:bookId')
async getBook(('bookId', new JoiPipe(Joi.string().required())) bookId: string) {
// bookId guaranteed to be a string and defined and non-empty
return this.bookService.getBookById(bookId);
}
```
### Pipe options (`pipeOpts`)
Currently, the following options are available:
- `group` (`string | symbol`) When a `group` is defined, only decorators specified for that group or the `DEFAULT` group when declaring the schema will be used to construct the schema. **Default:** `undefined`
- `usePipeValidationException` (`boolean`) By default, `JoiPipe` throws a NestJS `BadRequestException` when a validation error occurs. This results in a `400 Bad Request` response, which should be suitable to most cases. If you need to have a reliable way to catch the thrown error, for example in an exception filter, set this to `true` to throw a `JoiPipeValidationException` instead. **Default:** `false`
- `defaultValidationOptions` (`Joi.ValidationOptions`) The default Joi validation options to pass to `.validate()`
- **Default:** `{ abortEarly: false, allowUnknown: true }`
- Note that validation options passed directly to a schema using `.prefs()` (or `.options()`) will **always** take precedence and can never be overridden with this option.
- `skipErrorFormatting` (`boolean`) By default, `JoiPipe` returns a formatted readable error message. If you need to handle error message formatting setting this to `true` will return the original error. **Default:** `false`
### Injection-enabled mode: `JoiPipe` (`(JoiPipe)`, `(JoiPipe)`, ...)
Uses an injection-enabled `JoiPipe` which can look at the request to determine the HTTP method and, based on that, which in-built group (`CREATE`, `UPDATE`, `DEFAULT`) to use.
Validates against the schema constructed from the `metatype`, if present, taking into account the group determined as stated above.
```typescript
export class BookDto {
(Joi.string().required())
([JoiValidationGroups.CREATE], Joi.string().required())
([JoiValidationGroups.UPDATE], Joi.string().optional())
name!: string;
(Joi.string().required())
([JoiValidationGroups.CREATE], Joi.string().required())
([JoiValidationGroups.UPDATE], Joi.string().optional())
author!: string;
(Joi.number().optional())
publicationYear?: number;
}
()
class BookController {
// POST: this will implicitely use the group "CREATE" to construct the schema
('/')
async createBook((JoiPipe) createData: BookDto) {
return await this.bookService.createBook(createData);
}
}
```
### Defining `pipeOpts` in injection-enabled mode
In injection-enabled mode, options cannot be passed to `JoiPipe` directly since the constructor is passed as an argument instead of an instance, which would accept the `pipeOpts` argument.
Instead, the options can be defined by leveraging the DI mechanism itself to provide the options through a provider:
```typescript
({
...
controllers: [ControllerUsingJoiPipe],
providers: [
{
provide: JOIPIPE_OPTIONS,
useValue: {
usePipeValidationException: true,
},
},
],
...
})
export class AppModule {}
```
**Note**: the provider must be defined on the correct module to be "visible" in the DI context in which the `JoiPipe` is being injected. Alternatively, it can be defined and exported in a global module. See [the NestJS documentation for this](https://docs.nestjs.com/modules).
For how to define options when using the `JoiPipeModule`, [refer to the section on `JoiPipeModule` below](#joipipemodule).
### Error handling and custom schema errors
As described in the [`pipeOpts`](#pipe-options-pipeopts), when a validation error occurs, `JoiPipe` throws a `BadRequestException` or a `JoiPipeValidationException` (if configured).
If your schema defines a custom error, that error will be thrown instead:
```typescript
(
Joi.string()
.required()
.alphanum()
.error(
new Error(
`prop must contain only alphanumeric characters`,
),
),
)
prop: string;
```
## `JoiPipeModule`
Importing `JoiPipeModule` into a module will install `JoiPipe` as a global injection-enabled pipe.
This is a prerequisite for `JoiPipe` to be able to use the built-in groups `CREATE` and `UPDATE`, since the `JoiPipe` must be able to have the `Request` injected to determine the HTTP method. Calling `useGlobalPipe(new JoiPipe())` is not enough to achieve that.
**Example**
```typescript
import { value JoiPipeModule } from 'nestjs-joi';
({
controllers: [BookController],
imports: [JoiPipeModule],
})
export class AppModule {}
//
// Equivalent to:
import { value JoiPipe } from 'nestjs-joi';
({
controllers: [BookController],
providers: [
{
provide: APP_PIPE,
useClass: JoiPipe,
},
],
})
export class AppModule {}
```
Pipe options (`pipeOpts`) can be passed by using `JoiPipeModule.forRoot()`:
```typescript
import { value JoiPipeModule } from 'nestjs-joi';
({
controllers: [BookController],
imports: [
JoiPipeModule.forRoot({
pipeOpts: {
usePipeValidationException: true,
},
}),
],
})
export class AppModule {}
//
// Equivalent to:
import { value JoiPipe } from 'nestjs-joi';
({
controllers: [BookController],
providers: [
{
provide: APP_PIPE,
useClass: JoiPipe,
},
{
provide: JOIPIPE_OPTIONS,
useValue: {
usePipeValidationException: true,
},
},
],
})
export class AppModule {}
```
## `JoiPipeValidationException`
Thrown instead of a `BadRequestException` if the `usePipeValidationException` option for `JoiPipe` is set to `true`.
**Properties**
- `message`: a formatted message, or the native `message` property value from the `Joi.ValidationError` if the `skipErrorFormatting` option for `JoiPipe` is set to `true`.
- `joiValidationError`: the native `Joi.ValidationError` thrown by Joi.
## `JoiPipeValidationRpcExceptionFilter`
**Not exported from `nestjs-joi` to prevent a dependency on `/microservice`!**
```typescript
import { JoiPipeValidationRpcExceptionFilter } from 'nestjs-joi/microservice';
```
Exception filter that can be used in a microservice module to catch `JoiPipeValidationException` exceptions and re-throw them as `RpcException`, preventing NestJS from swallowing it quietly and turning it into a generic "Internal server error". **Note** that the RpcException does not save over the stack trace.
## `()` property decorator
Define a schema on a type (class) property. Properties with a schema annotation are used to construct a full object schema.
[**API documentation in `joi-class-decorators` repository.**](https://github.com/whefter/joi-class-decorators#joischema-property-decorator)
## `()` class decorator
Assign the passed Joi _options_ to be passed to `.options()` on the full constructed schema.
[**API documentation in `joi-class-decorators` repository.**](https://github.com/whefter/joi-class-decorators#joischemaoptions-class-decorator)
## `(type)` class decorator
Specify an alternative extended class for schema construction. `type` must be a class constructor.
[**API documentation in `joi-class-decorators` repository.**](https://github.com/whefter/joi-class-decorators#joischemaextendstype-class-decorator)
## `(callback)` class decorator
Specify a customization function for the final constructed type schema. `callback` must be a function that takes a schema and returns a modified schema.
[**API documentation in `joi-class-decorators` repository.**](https://github.com/whefter/joi-class-decorators#joischemacustomization-class-decorator)
## `getClassSchema(typeClass, opts?: { group? })` (alias: `getTypeSchema()`)
This function can be called to obtain the `Joi` schema constructed from `type`. This is the function used internally by `JoiPipe` when it is called with an explicit/implicit type/metatype. Nothing is cached.
[**API documentation in `joi-class-decorators` repository.**](https://github.com/whefter/joi-class-decorators#getclassschematypeclass-opts--group--alias-gettypeschema)