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 version 8, 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 option | Configuration key | FoalTS default | 
|---|---|---|
| $data | settings.ajv.$data | - | 
| allErrors | settings.ajv.allErrors | - | 
| coerceTypes | settings.ajv.coerceType | true | 
| removeAdditional | settings.ajv.removeAdditional | true | 
| useDefaults | settings.ajv.useDefaults | true | 
Example
- YAML
- JSON
- JS
settings:
  ajv:
    coerceTypes: true
{
  "settings": {
    "ajv": {
      "coerceTypes": true
    }
  }
}
module.exports = {
  settings: {
    ajv: {
      coerceTypes: true
    }
  }
}
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": [
    {
      "instancePath": "/price",
      "keyword": "type",
      "message": "must be integer",
      "params": {
        "type": "integer"
      },
      "schemaPath": "#/properties/price/type"
    }
  ]
}
ValidateHeader
It validates the request headers (Context.request.headers).
HTTP request
GET /products
Authorization: xxx
A-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() {
    // ...
  }
}
HTTP response (400 - BAD REQUEST)
{
  "headers": [
    {
      "instancePath": "/a-number",
      "keyword": "type",
      "message": "must be integer",
      "params": {
        "type": "integer"
      },
      "schemaPath": "#/properties/a-number/type"
    }
  ]
}
ValidateCookie
It validates the request cookies (Context.request.cookies).
HTTP request
GET /products
Cookies: 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() {
    // ...
  }
}
HTTP response (400 - BAD REQUEST)
{
  "cookies": [
    {
      "instancePath": "/A-Number",
      "keyword": "type",
      "message": "must be integer",
      "params": {
        "type": "integer"
      },
      "schemaPath": "#/properties/A-Number/type"
    }
  ]
}
ValidatePathParam
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() {
    // ...
  }
}
HTTP response (400 - BAD REQUEST)
{
  "pathParams": [
    {
      "instancePath": "/productId",
      "keyword": "type",
      "message": "must be integer",
      "params": {
        "type": "integer"
      },
      "schemaPath": "#/properties/productId/type"
    }
  ]
}
ValidateQueryParam
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() {
    // ...
  }
}
HTTP response (400 - BAD REQUEST)
{
  "query": [
    {
      "instancePath": "/a-number",
      "keyword": "type",
      "message": "must be integer",
      "params": {
        "type": "integer"
      },
      "schemaPath": "#/properties/a-number/type"
    }
  ]
}
Sanitization Example
import { Context, HttpResponseOK, Post, ValidateBody } from '@foal/core';
export class AppController {
  @Post('/no-sanitization')
  noSanitization(ctx: Context) {
    return new HttpResponseOK(ctx.request.body);
  }
  @Post('/sanitization')
  @ValidateBody({
    additionalProperties: false,
    properties: {
      age: { type: 'number' },
      name: { type: 'string' },
    },
    required: [ 'name', 'age' ],
    type: 'object'
  })
  sanitization(ctx: Context) {
    return new HttpResponseOK(ctx.request.body);
  }
}
Assuming that you did not change Foal's default configuration of Ajv (see above), you will get these results:
| Request | Response | 
|---|---|
| POST /no-sanitization{ name: 'Alex', age: '34', city: 'Paris' } | { name: 'Alex', age: '34', city: 'Paris' } | 
| POST /sanitization{ name: 'Alex', age: '34', city: 'Paris' } | { name: 'Alex', age: 34 } | 
Custom Error Messages
npm install ajv-errors@3 ajv@8
You can customize the errors returned by the validation hooks by using the ajv-errors plugin.
Configuration
- YAML
- JSON
- JS
settings:
  ajv:
    allErrors: true
{
  "settings": {
    "ajv": {
      "allErrors": true
    }
  }
}
module.exports = {
  settings: {
    ajv: {
      allErrors: true
    }
  }
}
Example
import { Context, getAjvInstance, HttpResponseOK, Post, ValidateBody } from '@foal/core';
import ajvErrors from 'ajv-errors';
export class AppController {
  init() {
    ajvErrors(getAjvInstance());
  }
  @Post('/products')
  @ValidateBody({
    additionalProperties: false,
    errorMessage: 'The submitted product is incorrect.',
    properties: {
      name: { type: 'string' }
    },
    required: [ 'name' ],
    type: 'object',
  })
  createProduct(ctx: Context) {
    // ...
    return new HttpResponseOK(ctx.request.body);
  }
}
Referencing Schemas
The example below shows how a schema can be defined and reused in several hooks.
Example
import { Context, getAjvInstance, HttpResponseOK, Post, ValidateBody } from '@foal/core';
const productSchema = {
  additionalProperties: false,
  properties: {
    name: { type: 'string' }
  },
  required: [ 'name' ],
  type: 'object',
};
export class ProductController {
  boot() {
    getAjvInstance()
      .addSchema(productSchema, 'Product');
  }
  @Post('/products')
  @ValidateBody({
    $ref: 'Product'
  })
  createProduct(ctx: Context) {
    // ...
    return new HttpResponseOK(ctx.request.body);
  }
}
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 pass
post.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@0.5 class-validator@0.14 @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": { "isLength": "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;
}