Skip to main content
Version: v3

Controllers

foal generate controller my-controller
import { Context, Get, HttpResponseOK } from '@foal/core';

export class ProductController {

@Get('/products')
listProducts(ctx: Context) {
return new HttpResponseOK([]);
}

}

Description

Controllers are the front door of your application. They intercept all incoming requests and return the responses to the client.

The code of a controller should be concise. If necessary, controllers can delegate some tasks to services (usually the business logic).

Controller Architecture

A controller is simply a class of which some methods are responsible for a route. These methods must be decorated by one of theses decorators Get, Post, Patch, Put, Delete, Head or Options. They may be asynchronous.

Example:

import { Get, HttpResponseOK, Post } from '@foal/core';

class MyController {
@Get('/foo')
foo() {
return new HttpResponseOK('I\'m listening to GET /foo requests.');
}

@Post('/bar')
bar() {
return new HttpResponseOK('I\'m listening to POST /bar requests.');
}
}

Controllers may have sub-controllers declared in the subControllers property.

Example:

import { controller, Get, HttpResponseOK, Post } from '@foal/core';

class MySubController {
@Get('/foo')
foo() {
return new HttpResponseOK('I\'m listening to GET /barfoo/foo requests.');
}
}

class MyController {
subControllers = [
controller('/barfoo', MySubController)
]

@Post('/bar')
bar() {
return new HttpResponseOK('I\'m listening to POST /bar requests.');
}
}

The AppController

The AppController is the main controller of your application. It is directly bound to the request handler. Every controller must be, directly or indirectly, a sub-controller of the AppController.

Example:

import { controller, IAppController } from '@foal/core';

import { ApiController } from './controllers/api.controller';

export class AppController implements IAppController {
subControllers = [
controller('/api', ApiController)
];
}

Contexts & HTTP Requests

The Context object

On every request, the controller method is called with a Context object. This context is unique and specific to the request.

It has seven properties:

NameTypeDescription
requestRequestGives information about the HTTP request.
state{ [key: string]: any }Object which can be used to forward data accross several hooks (see Hooks).
user{ [key: string]: any }|nullThe current user (see Authentication).
sessionSession|nullThe session object if you use sessions.
filesFileListA list of file paths or buffers if you uploaded files (see Upload and download files).
controllerNamestringThe name of the controller class.
controllerMethodNamestringThe name of the controller method.

The types of the user and state properties are generic. You override their types if needed:

import { Context, Get } from '@foal/core';

interface State {
foo: string;
}

interface User {
id: string;
}

export class ProductController {
@Get('/')
getProducts(ctx: Context<User, State>) {
// ...
}
}

HTTP Requests

The request property is an ExpressJS request object. Its complete documentation can be consulted here. The below sections detail common use cases.

Read the Body

The request body is accessible with the body attribute. Form data and JSON objects are automatically converted to JavaScript objects in FoalTS.

POST /products

{
"name": "milk"
}
import { Context, HttpResponseCreated, Post } from '@foal/core';

class AppController {
@Post('/products')
createProduct(ctx: Context) {
const body = ctx.request.body;
// Do something.
return new HttpResponseCreated();
}
}

Read Path Parameters

Path parameters are accessible with the params attribute.

GET /products/3
import { Context, HttpResponseOK, Get } from '@foal/core';

class AppController {
@Get('/products/:id')
readProduct(ctx: Context) {
const productId = ctx.request.params.id;
// Do something.
return new HttpResponseOK();
}
}

Read Query Parameters

Query parameters are accessible with the query attribute.

GET /products?limit=3
import { Context, HttpResponseOK, Get } from '@foal/core';

class AppController {
@Get('/products')
readProducts(ctx: Context) {
const limit = ctx.request.query.limit;
// Do something.
return new HttpResponseOK();
}
}

Read Headers

Headers are accessible with the get method.

import { Context, HttpResponseOK, Get } from '@foal/core';

class AppController {
@Get('/')
index(ctx: Context) {
const token = ctx.request.get('Authorization');
// Do something.
return new HttpResponseOK();
}
}

Read Cookies

Cookies are accessible through the cookies attribute.

import { Context, HttpResponseOK, Get } from '@foal/core';

class AppController {
@Get('/')
index(ctx: Context) {
const sessionID: string|undefined = ctx.request.cookies.sessionID;
// Do something.
return new HttpResponseOK();
}
}

Signed cookies are accessible through the signedCookies attribute.

import { Context, HttpResponseOK, Get } from '@foal/core';

class AppController {
@Get('/')
index(ctx: Context) {
const cookie1: string|undefined = ctx.request.signedCookies.cookie1;
// Do something.
return new HttpResponseOK();
}
}

In order to use signed cookies, you must provide a secret with the configuration key settings.cookieParser.secret.

The Controller Method Arguments

The path paramaters and request body are also passed as second and third arguments to the controller method.

import { Context, HttpResponseCreated, Put } from '@foal/core';

class AppController {
@Put('/products/:id')
updateProduct(ctx: Context, { id }, body) {
// Do something.
return new HttpResponseCreated();
}
}

HTTP Responses

HTTP responses are defined using HttpResponse objects. Each controller method must return an instance of this class (or a promise of this instance).

Here are subclasses that you can use:

HTTP methodResponse classIs abstract?
2XX Success
2XXHttpResponseSuccessyes
200HttpResponseOKno
201HttpResponseCreatedno
3XX Redirection
3XXHttpResponseRedirectionyes
301HttpResponseMovedPermanentlyno
302HttpResponseRedirectno
4XX Client errors
4XXHttpResponseClientErroryes
400HttpResponseBadRequestno
401HttpResponseUnauthorizedno
403HttpResponseForbiddenno
404HttpResponseNotFoundno
405HttpResponseMethodNotAllowedno
409HttpResponseConflictno
429HttpResponseTooManyRequestsno
5XX Server errors
5XXHttpResponseServerErroryes
500HttpResponseInternalServerErrorno
501HttpResponseNotImplementedno

Most of these responses accept a body at instantiation. It can be a Buffer object, a string, an object, a number, an array, or even a Node.JS stream.

Example with a body

new HttpResponseBadRequest({
message: 'The foo field is missing.'
})

In case the body parameter is a stream, you must specify it using the stream option.

Example with a Node.JS stream as body

new HttpResponseOK(myStream, { stream: true })

The HttpResponseServerError constructor also accepts two other options: a Context object and an error.

Example

new HttpResponseServerError({}, { error, ctx });

The type of the body may be constrained. This is useful if you wish to guarantee your endpoints return a certain data shape.

Example with a constrained body type

interface Item {
title: string
}

// OK
new HttpResponseOK<Item>({ title: 'foobar' })

// Error
new HttpResponseOK<Item>('foobar')

Adding Headers

Example

import { Get, HttpResponseOK } from '@foal/core';

class AppController {
@Get('/')
index() {
return new HttpResponseOK()
.setHeader('Cache-Control', 'max-age=604800, public');
}
}

Adding Cookies

Example with no cookie directives

import { Get, HttpResponseOK } from '@foal/core';

class AppController {
@Get('/')
index() {
return new HttpResponseOK()
.setCookie('state', 'foobar');
}
}

Example with cookie directives

import { Get, HttpResponseOK } from '@foal/core';

class AppController {
@Get('/')
index() {
return new HttpResponseOK()
.setCookie('sessionID', 'xxxx', {
domain: 'example.com',
httpOnly: true,
// expires: new Date(2022, 12, 12),
maxAge: 3600,
path: '/',
sameSite: 'lax',
secure: true,
});
}
}

The maxAge cookie directive defines the number of seconds until the cookie expires.

Example with a signed cookie.

import { Get, HttpResponseOK } from '@foal/core';

class AppController {
@Get('/')
index() {
return new HttpResponseOK()
.setCookie('cookie1', 'value1', {
signed: true
});
}
}

In order to use signed cookies, you must provide a secret with the configuration key settings.cookieParser.secret.

Testing Controllers

A controller is a simple class and so can be tested as is. Note that hooks are ignored upon testing.

api.controller.ts (example)

import { Context, Get, HttpResponseOK } from '@foal/core';
import { JWTRequired } from '@foal/jwt';

class ApiController {
@Get('/users/me')
@JWTRequired()
getCurrentUser(ctx: Context) {
return new HttpResponseOK(ctx.user);
}
}

api.controller.spec.ts (example)

import { strictEqual } from 'assert';

import { Context, createController, HttpResponseOK, isHttpResponseOK } from '@foal/core';

import { ApiController } from './api.controller';

describe('ApiController', () => {

it('should return the current user.', () => {
// Instantiate the controller.
const controller = createController(ApiController);

// Create a fake user (the current user)
const user = { name: 'Alix' };

// Create a fake Context object to simulate the request.
const ctx = new Context({}); // "{}" is the request body.
ctx.user = user;

// Execute the controller method and save the response.
const response = controller.getCurrentUser(ctx);

if (!isHttpResponseOK(response)) {
throw new Error('The response should be an HttpResponseOK');
}

strictEqual(response.body, user);
});

});

Due to the way packages are managed by npm, you should always use isHttpResponseOK(response) rather than response instanceof HttpResponseOK to avoid reference bugs.

Inheriting Controllers

Example:

import { Get, HttpResponseOK, Post } from '@foal/core';

abstract class ParentController {
@Get('/foo')
foo() {
return new HttpResponseOK();
}
}


class ChildController extends ParentController {
@Post('/bar')
bar() {
return new HttpResponseOK();
}
}

You can also override foo. If you don't add a Get, Post, Patch, Put, Delete, Head or Options decorator then the parent path and HTTP method are used. If you don't add a hook, then the parent hooks are used. Otherwise they are all discarded.