Saltar al contenido principal
Version: 1.x

Validation & Sanitization

Validation checks if an input meets a set of criteria (such as the value of a property is a string).

Sanitization modifies the input to ensure that it is valid (such as coercing a type).

Foal offers several utils and hooks to handle both validation and sanitization. They are particularly useful for checking and transforming parts of HTTP requests (such as the body).

With a JSON Schema (AJV)#

Ajv, the JSON Schema Validator#

FoalTS default validation and sanitization system is based on Ajv, a fast JSON Schema Validator. You'll find more details on how to define a shema on its website.

Options#

Here is the list of AJV options that can be overridden with FoalTS configuration system.

Ajv optionConfiguration keyFoalTS default
coerceTypessettings.ajv.coerceTypetrue
removeAdditionalsettings.ajv.removeAdditionaltrue
useDefaultssettings.ajv.useDefaultstrue
nullablesettings.ajv.nullable/
allErrorssettings.ajv.allErrors/

Example: config/default.json

{  "settings": {    "ajv": {      "coerceTypes": true    }  }}

The validate util#

The validate util throws a ValidationError if the given data does not fit the shema.

Example

import { validate } from '@foal/core';
const schema = {  properties: {    a: { type: 'number' }  },  type: 'object'};const data = {  a: 'foo'};
validate(schema, data);// => Throws an error (ValidationError)// => error.content contains the details of the validation error.

Validation & Sanitization of HTTP Requests#

FoalTS provides many hooks to validate and sanitize HTTP requests. When validation fails, they return an HttpResponseBadRequest object whose body contains the validation errors.

Example

import { Context, HttpResponseOK, Post, ValidateBody } from '@foal/core';
export class MyController {
  @Post('/user')  @ValidateBody({    additionalProperties: false,    properties: {      firstName: { type: 'string' },      lastName: { type: 'string' },    },    required: [ 'firstName', 'lastName' ],    type: 'object'  })  postUser(ctx: Context) {    // In this method we are sure that firstName and lastName    // are defined thanks to the above hook.    console.log(      ctx.request.body.firstName, ctx.request.body.lastName    );    return new HttpResponseOK();  }
}

ValidateBody#

It validates the request body (Context.request.body).

HTTP request

POST /products
{  "price": "hello world"}

Controller (first example)

import { Post, ValidateBody } from '@foal/core';
export class AppController {  @Post('/products')  @ValidateBody({    additionalProperties: false,    properties: {      price: { type: 'integer' },    },    required: [ 'price' ],    type: 'object'  })  createProduct() {    // ...  }}

Controller (second example)

import { Post, ValidateBody } from '@foal/core';
export class AppController {  schema = {    additionalProperties: false,    properties: {      price: { type: 'integer' },    },    required: [ 'price' ],    type: 'object'  };
  @Post('/products')  @ValidateBody(controller => controller.schema)  createProduct() {    // ...  }}

HTTP response (400 - BAD REQUEST)

{  "body": [    {      "dataPath": ".price",      "keyword": "type",      "message": "should be integer",      "params": {        "type": "integer"      },      "schemaPath": "#/properties/price/type"    }  ]}

ValidateHeader & ValidateHeaders#

It validates the request headers (Context.request.headers).

HTTP request

GET /productsAuthorization: xxxA-Number: hello

Controller (first example)

import { Post, ValidateHeader } from '@foal/core';
export class AppController {  @Get('/products')  @ValidateHeader('Authorization')  @ValidateHeader('A-Number', { type: 'integer' }, { required: false })  readProducts() {    // ...  }}

Controller (second example)

import { Post, ValidateHeader } from '@foal/core';
export class AppController {  schema = { type: 'integer' };
  @Get('/products')  @ValidateHeader('Authorization')  @ValidateHeader('A-Number', c => c.schema, { required: false })  readProducts() {    // ...  }}

Controller (third example)

import { Post, ValidateHeaders } from '@foal/core';
export class AppController {  @Get('/products')  // Deprecated since v1.12. Use @ValidateHeader instead.  @ValidateHeaders({    properties: {      // All properties should be in lower case.      'a-number': { type: 'integer' },      'authorization': { type: 'string' },    },    required: [ 'authorization' ],    type: 'object'  })  readProducts() {    // ...  }}

HTTP response (400 - BAD REQUEST)

{  "headers": [    {      "dataPath:" "['a-number']",      "keyword": "type",      "message": "should be integer",      "params": {        "type": "integer"      },      "schemaPath": "#/properties/a-number/type"    }  ]}

ValidateCookie & ValidateCookies#

It validates the request cookies (Context.request.cookies).

HTTP request

GET /productsCookies: Authorization=xxx; A-Number=hello

Controller (first example)

import { Post, ValidateCookie } from '@foal/core';
export class AppController {  @Get('/products')  @ValidateCookie('Authorization')  @ValidateCookie('A-Number', { type: 'integer' }, { required: false })  readProducts() {    // ...  }}

Controller (second example)

import { Post, ValidateCookie } from '@foal/core';
export class AppController {  schema = { type: 'integer' };
  @Get('/products')  @ValidateCookie('Authorization')  @ValidateCookie('A-Number', c => c.schema, { required: false })  readProducts() {    // ...  }}

Controller (third example)

import { Post, ValidateCookies } from '@foal/core';
export class AppController {  @Get('/products')  @Hook(ctx => console.log(ctx.request.cookies))  // Deprecated since v1.12. Use @ValidateCookie instead.  @ValidateCookies({    properties: {      'A-Number': { type: 'integer' },      'Authorization': { type: 'string' },    },    required: [ 'Authorization' ],    type: 'object'  })  readProducts() {    // ...  }}

HTTP response (400 - BAD REQUEST)

{  "cookies": [    {      "dataPath": "['a-number']",      "keyword": "type",      "message": "should be integer",      "params": {        "type": "integer"      },      "schemaPath": "#/properties/a-number/type"    }  ]}

ValidatePathParam & ValidateParams#

It validates the request path parameter (Context.request.params).

HTTP request

GET /products/xxx

Controller (first example)

import { Post, ValidatePathParam } from '@foal/core';
export class AppController {  @Get('/products/:productId')  @ValidatePathParam('productId', { type: 'integer' })  readProducts() {    // ...  }}

Controller (second example)

import { Post, ValidatePathParam } from '@foal/core';
export class AppController {  schema = { type: 'integer' };
  @Get('/products/:productId')  @ValidatePathParam('productId', c => c.schema)  readProducts() {    // ...  }}

Controller (third example)

import { Post, ValidateParams } from '@foal/core';
export class AppController {  @Get('/products/:productId')  // Deprecated since v1.12. Use @ValidatePathParam instead.  @ValidateParams({    properties: {      productId: { type: 'integer' }    },    type: 'object'  })  readProducts() {    // ...  }}

HTTP response (400 - BAD REQUEST)

{  "pathParams": [    {      "dataPath": ".productId",      "keyword": "type",      "message": "should be integer",      "params": {        "type": "integer"      },      "schemaPath": "#/properties/productId/type"    }  ]}

ValidateQueryParam & ValidateQuery#

It validates the request query (Context.request.query).

HTTP request

GET /products?authorization=xxx&a-number=hello

Controller (first example)

import { Post, ValidateQueryParam } from '@foal/core';
export class AppController {  @Get('/products')  @ValidateQueryParam('authorization')  @ValidateQueryParam('a-number', { type: 'integer' }, { required: false })  readProducts() {    // ...  }}

Controller (second example)

import { Post, ValidateQueryParam } from '@foal/core';
export class AppController {  schema = { type: 'integer' };
  @Get('/products')  @ValidateQueryParam('authorization')  @ValidateQueryParam('a-number', c => c.schema, { required: false })  readProducts() {    // ...  }}

Controller (third example)

import { Post, ValidateQuery } from '@foal/core';
export class AppController {  @Get('/products')  // Deprecated since v1.12. Use @ValidateQueryParam instead.  @ValidateQuery({    properties: {      'a-number': { type: 'integer' },      'authorization': { type: 'string' },    },    required: [ 'authorization' ],    type: 'object'  })  readProducts() {    // ...  }}

HTTP response (400 - BAD REQUEST)

{  "query": [    {      "dataPath": "['a-number']",      "keyword": "type",      "message": "should be integer",      "params": {        "type": "integer"      },      "schemaPath": "#/properties/a-number/type"    }  ]}

Sanitization Example#

import { Context, Get, HttpResponseOK, ValidateQuery } from '@foal/core';
export class AppController {
  @Get('/no-sanitization')  noSanitization(ctx: Context) {    return new HttpResponseOK(ctx.request.query);  }
  @Get('/sanitization')  @ValidateQuery({    additionalProperties: false,    properties: {      apiKey: { type: 'number' },      name: { type: 'string' },    },    required: [ 'name', 'apiKey' ],    type: 'object'  })  sanitization(ctx: Context) {    return new HttpResponseOK(ctx.request.query);  }
}

Assuming that you did not change Foal's default configuration of Ajv (see above), you will get these results:

RequestResponse
GET /no-sanitization?name=Alex&apiKey=34&city=Paris{ name: 'Alex', apiKey: '34', city: 'Paris' }
GET /sanitization?name=Alex&apiKey=34&city=Paris{ name: 'Alex', apiKey: 34 }

With a Validation Class (class-validator)#

The class-validator library can also be used in Foal to validate an object against a validation class.

npm install class-validator

Example

import {validate, Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, Min, Max} from "class-validator"; export class Post {
    @IsInt()    @Min(0)    @Max(10)    rating: number;     @IsEmail()    email: string; } let post = new Post();post.rating = 11; // should not passpost.email = "google.com"; // should not pass validate(post).then(errors => { // errors is an array of validation errors    if (errors.length > 0) {        console.log("validation failed. errors: ", errors);    } else {        console.log("validation succeed");    }});

Usage with a Hook#

npm install class-transformer class-validator @foal/typestack

If you want to use it within a hook to validate request bodies, you can install the package @foal/typestack for this. It provides a @ValidateBody hook that validates the body against a given validator. This body is also unserialized and turned into an instance of the class.

social-post.validator.ts

import { Contains, Length } from 'class-validator';
export class SocialPost {
  @Length(10, 20)  title: string;
  @Contains('hello')  text: string;
}

social-post.controller.ts (first example)

import { Context, HttpResponseCreated, Post } from '@foal/core';import { ValidateBody } from '@foal/typestack';import { SocialPost } from './social-post.validator';
export class SocialPostController {
  @Post()  @ValidateBody(SocialPost, { /* options if relevant */ })  createSocialPost(ctx: Context) {    // ctx.request.body is an instance of SocialPost.    // ...    return new HttpResponseCreated();  }
}

social-post.controller.ts (second example)

import { Context, HttpResponseCreated, Post } from '@foal/core';import { ValidateBody } from '@foal/typestack';import { SocialPost } from './social-post.validator';
export class SocialPostController {  entityClass = SocialPost;
  @Post()  @ValidateBody(controller => controller.entityClass, { /* options if relevant */ })  createSocialPost(ctx: Context) {    // ctx.request.body is an instance of SocialPost.    // ...    return new HttpResponseCreated();  }
}

HTTP request (example)

POST /
{  "text": "foo"}

HTTP response (example)

[  {    "children": [],    "constraints": { "length": "title must be longer than or equal to 10 characters" },    "property": "title",    "target": { "text": "foo" },  },  {    "children": [],    "constraints": { "contains": "text must contain a hello string" },    "property": "text",    "target": { "text": "foo" },    "value": "foo",  }]

The hook takes also an optional parameter to specify the options of the class-transformer and class-validator libraries.

Usage with TypeORM entities#

The validation decorators are compatible with TypeORM entities. So you can use one single class to define both your model and validation rules.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';import { Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, Min, Max } from 'class-validator';
@Entity()export class Post {
    @PrimaryGeneratedColumn()    id: number;        @Column()    @Length(10, 20)    title: string;
    @Column()    @Contains("hello")    text: string;
    @Column()    @IsInt()    @Min(0)    @Max(10)    rating: number;
    @Column()    @IsEmail()    email: string;
    @Column()    @IsFQDN()    site: string;
    @Column()    @IsDate()    createDate: Date;
}