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

The 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.

Foal uses this baseline ajv configuration under the hood:

{
coerceTypes: true, // change data type of data to match type keyword
removeAdditional: true, // remove additional properties
useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
}

You can override these settings with the config file config/default.json or using the environment variables SETTINGS_AJV_COERCE_TYPE, SETTINGS_AJV_REMOVE_ADDITIONAL, SETTINGS_AJV_USE_DEFAULTS and SETTINGS_AJV_NULLABLE.

Example

{
"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

This section describes changes introduced in version 1.0.0. Instructions to upgrade to the new release can be found here. Old documentation can be found here.

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 /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() {
// ...
}
}

Controller (third example)

import { Post, ValidateHeaders } from '@foal/core';
export class AppController {
@Get('/products')
@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 /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() {
// ...
}
}

Controller (third example)

import { Post, ValidateCookies } from '@foal/core';
export class AppController {
@Get('/products')
@Hook(ctx => console.log(ctx.request.cookies))
@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')
@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')
@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 { Get, HttpResponseOK, ValidateQuery } from '@foal/core';
export class AppController {
@Get('/no-sanitization')
noSanitization(ctx) {
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) {
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:

Request

Response

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 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 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

import { 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) {
// 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;
}