Easy validation with Nest.js and Joi

Romain Kelifa
6 min readMar 27, 2020

--

Hi everybody, hope you’re safe at home 🏠

Today a quick article about automated data validation with Joi and Nest.js !

This article assumes that you have basic knowledge about :

  • Nest.js and its dependency injection mechanism
  • Joi and automated data validation
  • Express and middlewares
  • TypeScript and Decorators feature

What is this all about ?

So typical use case here is to build an automated way to validate the data sent to your API routes.

That’s what Joi already provides as a feature, by allowing you to describe the structure of you data. Other libraries exist too of course like ajv, class-validator, and probably many others.

Typically you would define your data validation definition, or schema as they are called, this way :

import * as Joi from '@hapi/joi'
const schema = Joi.object({
mandatory: Joi.string().required() // some required string
optional: Joi.string().optional() // some optional string
})

That’s a very nice and declarative way to define what your data should look like, and allows for checking that any object abides by this definition.

What if I told you that it also comes bundled as a TypeScript Decorator thanks to Joiful ? Nice indeed 😌

This allows you to turn your previous definition into :

import * as Joiful from 'joiful'
class Schema {
@Joiful.string().required() // some required string
mandatory!: string
@Joiful.string().optional() // some optional string
optional?: string
}

As you can foresee, that might be nice to group many features directly in your classes definitions as decorators are by nature composables.

To illustrate my point you could think of, for example, combining with decorators from other libraries like TypeORM :

import * as Joi from 'joiful'
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class Client {
@PrimaryGeneratedColumn()
@Joiful.number().optional()
id?: number
@Column()
@Joiful.string().required()
mandatory!: string
@Column({ nullable: true })
@Joiful.string().optional()
optional?: string
}

What about being able to automatically forward everything sent to your Controller’s routes for validation ?

It’s actually called Pipes !

The documentation already state about implementing validation with Joi but the point here is to go a little bit further.

Let’s code !

Ok let’s build some basic, but illustrative, example 👐

Our example will implement :

  • automatic validation from a Joiful decorated class
  • automatic validation from a mix of Joi schema(s) and/or Joiful decorated class(es)

First let’s define some dummy validation schemas :

validation schemas, either implicit (top) or explicit (bottom)

Main goal is to have your route decorated this way :

  1. By automatically deducting the schema to validate with from the Joiful decorator class itself :
here schema is deducted from Joiful decorators annotations on Implicit class
  1. By providing either explicit Joiful decorated class(es) and/or Joi schema(s) :
here schemas of different kinds are defined as parameters of ValidationPipe (meant to be merged)

As we are good kids, let’s start by implementing it with Test-Driven-Development 😎

integration tests for our Nest.js validation pipe

available as a gist here

So here we are, let’s implement the Nest.js validation Pipe :

Nest.js validation pipe

available as a gist here

Let’s stop for a bit and check how this works 🤓

First Pipes have to implements the PipeTransform interface :

Pipe requires to implements PipeTransform

Which implies it requires a transform method :

PipeTransform required method

To get to the point quickly, it first checks if there’s any metadata.metatype (which is the case when providing it implicitly with a Joiful annotated class) or if it has been provided with schemas (when explicitly providing a bunch of mixed Joiful annotated classes and Joi schema definitions).

As you can see in this implementation explicit declaration will always take precedence over implicit one, but you could implement it differently of course.

Then if it’s explicitly defined :

will validate input whose schema is defined implicitly on the class with Joiful decorators

In this case if the value is an Array it might means an array containing items of the explicitly defined definitions, but the developer might want to have control over it after all, hence the wrapSchemaAsArray to give the developer the opportunity for example to directly provide some Joi.array(...) definition of his/her own.

Of course in that simple implementation, it is left to the developer to provide Joi schema definitions that are mergeable together.

Speaking about merging schemas, let’s have a look at how to do it :

merging all the schemas definitions provided inside ValidationPipe constructor

What happens here is that if we have a class annotated with Joiful decorators we retrieve its Joi definition via getJoiSchema, once left with Joi schemas we’re able to merge them together with concat.

Last but not least, we can differentiate Joi schema from Joiful annotated class by checking if propertyisJoi === true. As you can see here, Joiful annotated class type is infered to Constructor<any>, while Joi schema is infered as Joi.Schema.

Otherwise if it’s implicitly defined :

will valide input whose definitions are defined in ValidationPipe constructor

This case is a bit different of the previous one because it’s currently not possible to directly defineImplicit[] as a type in the route’s method signature. Not matter this limitation, it makes sense to assume that if it’s an Array it should implicitly validate each of its item with the class Joiful decorators.

note here the use of Implicit instead of Implicit[] even if we expect an Array of Implicit items : it will be handled correctly with Joiful.validateAsArrayClass(…) though

A special note too :

@Optional() decorator

The reason here to specify @Optional() decorator is that it makes constructor’s parameters optional and therefore allows you to use the Pipe with these 2 syntaxes :

flexible Pipe declarations

Otherwise you would be required to specify new ValidationPipe() even if you don’t intend to specify any parameter.

And that’s all folks ! 🐰🥕

Hope you enjoyed it,
Do not hesitate to either comment 💬 or show some love 👏 😉

Stay safe & happy coding 💻

--

--

Responses (2)