Page Contents

Overview

A Controller is a class that implements operations defined by application’s API. It implements an application’s business logic and acts as a bridge between the HTTP/REST API and domain/database models. Decorations are added to a Controller class and its members to map the API operations of the application to the corresponding controller’s operations. A Controller operates only on processed input and abstractions of backend services / databases.

This page will only cover a Controller’s usage with REST APIs.

Review questions

Simplest possible example of a Controller

  • app.controller()
  • a few methods
  • no usage of @api

How to create a basic Controller (beyond the hello world)

  • Using DI (@inject)
  • Using decorators (eg. @authenticate)
  • Defining routes via sugar annoations (@get, @post)
  • Errors
  • Using async / await and Promises

Operations

In the Operation example in Routes, the greet() operation was defined as a plain JavaScript function. The example below shows this as a Controller method in TypeScript.

// plain function Operation
function greet(name: string) {
  return `hello ${name}`;
}

// Controller method Operation
class MyController {
  greet(name: string) {
    return `hello ${name}`;
  }
}

Routing to Controllers

This is a basic API Specification used in the following examples. It is an Operation Object.

const spec = {
  parameters: [{name: 'name', schema: {type: 'string'}, in: 'query'}],
  responses: {
    '200': {
      description: 'greeting text',
      content: {
        'application/json': {
          schema: {type: 'string'},
        },
      },
    },
  },
};

There are several ways to define Routes to Controller methods. The first example defines a route to the Controller without any magic.

// ... in your application constructor
this.route('get', '/greet', spec, MyController, 'greet');

Decorators allow you to annotate your Controller methods with routing metadata, so LoopBack can call the app.route() function for you.

import {get} from '@loopback/rest';

class MyController {
  @get('/greet', spec)
  greet(name: string) {
    return `hello ${name}`;
  }
}

// ... in your application constructor
this.controller(MyController);

Specifying Controller APIs

For larger LoopBack applications, you can organize your routes into API Specifications using the OpenAPI specification. The @api decorator takes a spec with type ControllerSpec which comprises of a string basePath and a Paths Object Note that it is not the full OpenAPI specification.

// ... in your application constructor
this.api({
  openapi: '3.0.0',
  info: {
    title: 'Hello World App',
    version: '1.0.0',
  },
  paths: {
    '/greet': {
      get: {
        'x-operation-name': 'greet',
        'x-controller-name': 'MyController',
        parameters: [{name: 'name', schema: {type: 'string'}, in: 'query'}],
        responses: {
          '200': {
            description: 'greeting text',
            content: {
              'application/json': {
                schema: {type: 'string'},
              },
            },
          },
        },
      },
    },
  },
});
this.controller(MyController);

The @api decorator allows you to annotate your Controller with a specification, so LoopBack can call the app.api() function for you.

@api({
  openapi: '3.0.0',
  info: {
    title: 'Hello World App',
    version: '1.0.0',
  },
  paths: {
    '/greet': {
      get: {
        'x-operation-name': 'greet',
        'x-controller-name': 'MyController',
        parameters: [{name: 'name', schema: {type: 'string'}, in: 'query'}],
        responses: {
          '200': {
            description: 'greeting text',
            content: {
              'application/json': {
                schema: {type: 'string'},
              },
            },
          },
        },
      },
    },
  },
})
class MyController {
  greet(name: string) {
    return `hello ${name}`;
  }
}
app.controller(MyController);

Writing Controller methods

Below is an example Controller that uses several built in helpers (decorators). These helpers give LoopBack hints about the Controller methods.

import {HelloRepository} from '../repositories';
import {HelloMessage} from '../models';
import {get, param} from '@loopback/rest';
import {repository} from '@loopback/repository';

export class HelloController {
  constructor(
    @repository(HelloRepository) protected repository: HelloRepository,
  ) {}

  // returns a list of our objects
  @get('/messages')
  async list(@param.query.number('limit') limit = 10): Promise<HelloMessage[]> {
    if (limit > 100) limit = 100; // your logic
    return await this.repository.find({limit}); // a CRUD method from our repository
  }
}
  • HelloRepository extends from Repository, which is LoopBack’s database abstraction. See Repositories for more.
  • HelloMessage is the arbitrary object that list returns a list of.
  • @get('/messages') automatically creates the Paths Item Object for OpenAPI spec, which also handles request routing.
  • @param.query.number specifies in the spec being generated that the route takes a parameter via query which will be a number.

Handling Errors in Controllers

In order to specify errors for controller methods to throw, the class HttpErrors is used. HttpErrors is a class that has been re-exported from http-errors, and can be found in the @loopback/rest package.

Listed below are some of the most common error codes. The full list of supported codes is found here.

Status Code Error
400 BadRequest
401 Unauthorized
403 Forbidden
404 NotFound
500 InternalServerError
502 BadGateway
503 ServiceUnavailable
504 GatewayTimeout

The example below shows the previous controller revamped with HttpErrors along with a test to verify that the error is thrown properly.

// test/integration/controllers/hello.controller.integration.ts
import {HelloController} from '../../../src/controllers';
import {HelloRepository} from '../../../src/repositories';
import {testdb} from '../../fixtures/datasources/testdb.datasource';
import {expect} from '@loopback/testlab';
import {HttpErrors} from '@loopback/rest';

const HttpError = HttpErrors.HttpError;

describe('Hello Controller', () => {
  it('returns 422 Unprocessable Entity for non-positive limit', () => {
    const repo = new HelloRepository(testdb);
    const controller = new HelloController(repo);

    return expect(controller.list(0.4)).to.be.rejectedWith(HttpError, {
      message: 'limit is non-positive',
      statusCode: 422,
    });
  });
});
// src/controllers/hello.controller.ts
import {HelloRepository} from '../repositories';
import {HelloMessage} from '../models';
import {get, param, HttpErrors} from '@loopback/rest';
import {repository} from '@loopback/repository';

export class HelloController {
  constructor(
    @repository(HelloRepository) protected repo: HelloRepository,
  ) {}

  // returns a list of our objects
  @get('/messages')
  async list(@param.query.number('limit') limit = 10): Promise<HelloMessage[]> {
    // throw an error when the parameter is not a non-positive integer
    if (!Number.isInteger(limit) || limit < 1) {
      throw new HttpErrors.UnprocessableEntity('limit is non-positive');
    } else if (limit > 100) {
      limit = 100;
    }
    return await this.repo.find({limit});
  }
}