Skip to content

Latest commit

Β 

History

History
527 lines (409 loc) Β· 13.5 KB

File metadata and controls

527 lines (409 loc) Β· 13.5 KB
title ContrΓ΄leurs
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:

Name Type Description
request Request Gives 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 }|null The current user (see Authentication).
session Session|null The session object if you use sessions.
files FileList A list of file paths or buffers if you uploaded files (see Upload and download files).
controllerName string The name of the controller class.
controllerMethodName string The 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.

Read the request ID

On each request, a request ID is generated randomly. It can be read through ctx.request.id.

If the X-Request-ID header exists, then the header value is used as the request identifier.

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 method Response class Is abstract?
1XX Informational
1XX HttpResponseInformational yes
100 HttpResponseContinue no
101 HttpResponseSwitchingProtocols no
102 HttpResponseProcessing no
103 HttpResponseEarlyHints no
2XX Success
2XX HttpResponseSuccess yes
200 HttpResponseOK no
201 HttpResponseCreated no
203 HttpResponseNonAuthoritativeInformation no
205 HttpResponseResetContent no
206 HttpResponsePartialContent no
207 HttpResponseMultiStatus no
208 HttpResponseAlreadyReported no
226 HttpResponseIMUsed no
3XX Redirection
3XX HttpResponseRedirection yes
300 HttpResponseMultipleChoices no
301 HttpResponseMovedPermanently no
302 HttpResponseRedirect no
303 HttpResponseSeeOther no
304 HttpResponseNotModified no
307 HttpResponseTemporaryRedirect no
308 HttpResponsePermanentRedirect no
4XX Client errors
4XX HttpResponseClientError yes
400 HttpResponseBadRequest no
401 HttpResponseUnauthorized no
402 HttpResponsePaymentRequired no
403 HttpResponseForbidden no
404 HttpResponseNotFound no
405 HttpResponseMethodNotAllowed no
406 HttpResponseNotAcceptable no
407 HttpResponseProxyAuthenticationRequired no
408 HttpResponseRequestTimeout no
409 HttpResponseConflict no
410 HttpResponseGone no
411 HttpResponseLengthRequired no
412 HttpResponsePreconditionFailed no
413 HttpResponseContentTooLarge no
414 HttpResponseURITooLong no
415 HttpResponseUnsupportedMediaType no
416 HttpResponseRangeNotSatisfiable no
417 HttpResponseExpectationFailed no
418 HttpResponseImATeapot no
421 HttpResponseMisdirectedRequest no
422 HttpResponseUnprocessableContent no
423 HttpResponseLocked no
424 HttpResponseFailedDependency no
425 HttpResponseTooEarly no
426 HttpResponseUpgradeRequired no
429 HttpResponseTooManyRequests no
431 HttpResponseRequestHeaderFieldsTooLarge no
451 HttpResponseUnavailableForLegalReasons 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 })

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.