Version: 2.0.0 (latest)

Controllers

You are reading the documentation for version 2 of FoalTS. Instructions for upgrading to this version are available here. The old documentation can be found here.

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 four properties:

NameTypeDescription
requestRequestGives information about the HTTP request.
stateobjectObject which can be used to forward data accross several hooks (see Hooks).
user`anyundefined`
session`Sessionundefined`

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 with 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();
}
}

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 });

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.

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.