Version: 1.x

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 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, Post } from '@foal/core';
class AppController {
@Get('/products/:id')
readProduct(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')
readProducts(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;
// ...
}
}

The Controller Method Arguments#

Available in Foal v1.9.0 onwards.

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

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.