Controllers

foal generate controller my-controller
import { Context, Get, HttpResponseOK } from '@foal/core';
export class MyController {
@Get('/flights')
listFlights(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 { 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 } from '@foal/core';
import { ApiController } from './controllers/api.controller';
export class AppController {
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:

  • a request (type: Request) giving information about the HTTP request received,

  • a state (type: object) which can be used to share data between hooks (see Hooks),

  • a user (type: any or undefined) giving information on the current user (see Authentication),

  • and a session (type: Session or undefined) containing the session data if you use sessions.

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 requestBody = 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, Post } from '@foal/core';
class AppController {
@Get('/products/:id')
createProduct(ctx: Context) {
const productId = ctx.request.params.id;
// Do something.
return new HttpResponseOK(/* something */);
}
}

Read Query Parameters

Query parameters are accessible with the query attribute.

GET /products?limit=3
import { Context, HttpResponseOK, Post } from '@foal/core';
class AppController {
@Get('/products')
createProduct(ctx: Context) {
const limit = ctx.request.query.limit;
// Do something.
return new HttpResponseOK(/* something */);
}
}

Read Headers

Headers are accessible with the get method.

import { Context, Get } from '@foal/core';
class AppController {
@Get('/')
index(ctx: Context) {
const token: string|undefined = ctx.request.get('Authorization');
// ...
}
}

Read Cookies

Cookies are accessible with the cookies attribute.

import { Context, Get } from '@foal/core';
class AppController {
@Get('/')
index(ctx: Context) {
const sessionID: string|undefined = ctx.request.cookies.sessionID;
// ...
}
}

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 method

Response class

Is abstract?

2XX Success

2XX

HttpResponseSuccess

yes

200

HttpResponseOK

no

201

HttpResponseCreated

no

3XX Redirection

3XX

HttpResponseRedirection

yes

301

HttpResponseMovedPermanently

no

302

HttpResponseRedirect

no

4XX Client errors

4XX

HttpResponseClientError

yes

400

HttpResponseBadRequest

no

401

HttpResponseUnauthorized

no

403

HttpResponseForbidden

no

404

HttpResponseNotFound

no

405

HttpResponseMethodNotAllowed

no

409

HttpResponseConflict

no

5XX Server errors

5XX

HttpResponseServerError

yes

500

HttpResponseInternalServerError

no

501

HttpResponseNotImplemented

no

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

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'
// expires: new Date(2020, 12, 12),
httpOnly: true,
maxAge: 3600,
path: '/',
secure: true
sameSite: 'lax',
});
}
}

The maxAge cookie directive defines the number of seconds until the cookie expires. Versions prior to version 1 used milliseconds.

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)

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:

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.