nestjs-joi
Easy to use JoiPipe
as an interface between joi
and NestJS with optional decorator-based schema construction. Based on joi-class-decorators
.
DEFAULT
, CREATE
, UPDATE
JoiPipe
new JoiPipe(pipeOpts?)
new JoiPipe(type, pipeOpts?)
new JoiPipe(joiSchema, pipeOpts?)
pipeOpts
)JoiPipe
(@Query(JoiPipe)
, @Param(JoiPipe)
, ...)pipeOpts
in injection-enabled modeJoiPipeModule
JoiPipeValidationException
@JoiSchema()
property decorator@JoiSchemaOptions()
class decorator@JoiSchemaExtends(type)
class decorator@JoiSchemaCustomization(callback)
class decoratorgetClassSchema(typeClass, opts?: { group? })
(alias: getTypeSchema()
)npm install --save nestjs-joi
npm install --save @nestjs/common@^7 @nestjs/core@^7 joi@^17 reflect-metadata@^0.1
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 @JoiSchema()
, @JoiSchemaOptions()
, @JoiSchemaExtends()
decorators and the getTypeSchema()
function are re-exported from the joi-class-decorators
package.
import { JoiPipeModule, JoiSchema, JoiSchemaOptions, CREATE, UPDATE } from 'nestjs-joi';
import * as Joi from 'joi';
@Module({
controllers: [BookController],
imports: [JoiPipeModule],
})
export class AppModule {}
@JoiSchemaOptions({
allowUnknown: false,
})
export class BookDto {
@JoiSchema(Joi.string().required())
@JoiSchema([CREATE], Joi.string().required())
@JoiSchema([UPDATE], Joi.string().optional())
name!: string;
@JoiSchema(Joi.string().required())
@JoiSchema([CREATE], Joi.string().required())
@JoiSchema([UPDATE], Joi.string().optional())
author!: string;
@JoiSchema(Joi.number().optional())
publicationYear?: number;
}
@Controller('/books')
export class BookController {
@Post('/')
async createBook(@Body() createData: BookDto) {
// Validated creation data!
return await this.bookService.createBook(createData);
}
@Put('/')
async createBook(@Body() createData: BookDto) {
// Validated create data!
return await this.bookService.createBook(createData);
}
@Patch('/')
async createBook(@Body() 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.
@nestjs/graphql
This module can be used with @nestjs/graphql
, but with some caveats:
new JoiPipe()
to useGlobalPipes()
, @UsePipes()
, a pipe defined in @Args()
etc. works as expected.JoiPipe
constructor to useGlobalPipes()
, @UsePipes()
, @Args()
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 @nestjs/graphql
, which only processed GraphQL queries for if they are sent as GET
and POST
.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 @UsePipes()
or @Args()
.
To work around the issue of OmitType()
etc. breaking the inheritance chain for schema building, see @JoiSchemaExtends()
below.
@nestjs/microservice
This module can be used with @nestjs/microservice
, but with some caveats:
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.
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)@Catch()
) 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
.Groups can be used to annotate a property (@JoiSchema
) or class (@JoiSchemaOptions
) 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 section.
For more information, have a look at the validation groups documentation from joi-class-decorators
.
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 @Body(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:
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
) or for specific requests inside the @Param()
, @Query
etc. Request decorators.
When used with the the Request decorators, there are two possibilities:
JoiPipe
instanceJoiPipe
constuctor itself to leverage the injection and built-in group capabilitiesWhen 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.
@Controller('/books')
export class BookController {
@Post('/')
async createBook(@Body(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.
@Post('/')
async createBook(@Body(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.
@Post('/')
async createBook(@Body(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.
@Get('/:bookId')
async getBook(@Param('bookId', new JoiPipe(Joi.string().required())) bookId: string) {
// bookId guaranteed to be a string and defined and non-empty
return this.bookService.getBookById(bookId);
}
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()
{ abortEarly: false, allowUnknown: true }
.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
JoiPipe
(@Query(JoiPipe)
, @Param(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.
export class BookDto {
@JoiSchema(Joi.string().required())
@JoiSchema([JoiValidationGroups.CREATE], Joi.string().required())
@JoiSchema([JoiValidationGroups.UPDATE], Joi.string().optional())
name!: string;
@JoiSchema(Joi.string().required())
@JoiSchema([JoiValidationGroups.CREATE], Joi.string().required())
@JoiSchema([JoiValidationGroups.UPDATE], Joi.string().optional())
author!: string;
@JoiSchema(Joi.number().optional())
publicationYear?: number;
}
@Controller()
class BookController {
// POST: this will implicitely use the group "CREATE" to construct the schema
@Post('/')
async createBook(@Body(JoiPipe) createData: BookDto) {
return await this.bookService.createBook(createData);
}
}
pipeOpts
in injection-enabled modeIn 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:
@Module({
...
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.
For how to define options when using the JoiPipeModule
, refer to the section on JoiPipeModule
below.
As described in the 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:
@JoiSchema(
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
import { value JoiPipeModule } from 'nestjs-joi';
@Module({
controllers: [BookController],
imports: [JoiPipeModule],
})
export class AppModule {}
//
// Equivalent to:
import { value JoiPipe } from 'nestjs-joi';
@Module({
controllers: [BookController],
providers: [
{
provide: APP_PIPE,
useClass: JoiPipe,
},
],
})
export class AppModule {}
Pipe options (pipeOpts
) can be passed by using JoiPipeModule.forRoot()
:
import { value JoiPipeModule } from 'nestjs-joi';
@Module({
controllers: [BookController],
imports: [
JoiPipeModule.forRoot({
pipeOpts: {
usePipeValidationException: true,
},
}),
],
})
export class AppModule {}
//
// Equivalent to:
import { value JoiPipe } from 'nestjs-joi';
@Module({
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 @nestjs/microservice
!
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.
@JoiSchema()
property decoratorDefine 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.
@JoiSchemaOptions()
class decoratorAssign the passed Joi options to be passed to .options()
on the full constructed schema.
API documentation in joi-class-decorators
repository.
@JoiSchemaExtends(type)
class decoratorSpecify an alternative extended class for schema construction. type
must be a class constructor.
API documentation in joi-class-decorators
repository.
@JoiSchemaCustomization(callback)
class decoratorSpecify 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.
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.