Overview
A Controller is a class that implements operations defined by an 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.
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 this.repository.find({limit}); // a CRUD method from our repository } } HelloRepositoryextends fromRepository, which is LoopBack’s database abstraction. See Repositories for more.HelloMessageis the arbitrary object thatlistreturns a list of.@get('/messages')automatically creates the Paths Item Object for OpenAPI spec, which also handles request routing.@param.query.numberspecifies in the spec being generated that the route takes a parameter via query which will be a number.
Modifying Specifications Created by Controller Generator
You can run generator to create REST controllers with CRUD methods. The command and prompts are explained in page controller-generator. To modify the OpenAPI specifications of REST controllers, you can leverage the specification enhancers.
For example, the default naming convention for a path’s operationId is ${controllerName}.${methodName}. To override the operationId with a custom one ${controllerName}-${methodName}, you can define an enhancer as:
import {injectable} from '@loopback/core'; import { mergeOpenAPISpec, asSpecEnhancer, OASEnhancer, OpenApiSpec, } from '@loopback/rest'; /** * A spec enhancer to modify `operationId` in paths */ @injectable(asSpecEnhancer) export class OperationSpecEnhancer implements OASEnhancer { name = 'operationIdEnhancer'; // takes in the current spec, modifies it, and returns a new one modifySpec(spec: OpenApiSpec): OpenApiSpec { const paths = spec.paths; for (const path in paths) { for (const op in path) { const operationId = paths[path][op].operationId; // change operationId from 'MyController.MyMethod' to // 'MyController-MyMethod' if (operationId) paths[path][op].operationId = operationId.replace('.', '-'); } } return spec; } } Class factory to allow parameterized decorations
Since decorations applied on a top-level class cannot have references to variables, you can create a class factory that allows parameterized decorations as shown in the example below.
function createControllerClass(version: string, basePath: string) { @api({basePath: `${basePath}`}) class Controller { @get(`/${version}`) find() {} } } For a complete example, see parameterized-decoration.ts .
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.
src/tests/integration/controllers/hello.controller.integration.ts
import {HelloController} from '../../../controllers'; import {HelloRepository} from '../../../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 natural number limit', () => { const repo = new HelloRepository(testdb); const controller = new HelloController(repo); return expect(controller.list(0.4)).to.be.rejectedWith(HttpError, { message: 'limit is not a natural number', 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 natural number if (!Number.isInteger(limit) || limit < 1) { throw new HttpErrors.UnprocessableEntity('limit is not a natural number'); } else if (limit > 100) { limit = 100; } return this.repo.find({limit}); } } Creating Controllers at Runtime
A controller can be created for a model at runtime using the defineCrudRestController helper function from the @loopback/rest-crud package. It accepts a Model class and a CrudRestControllerOptions object. Dependency injection for the controller has to be configured by applying the inject decorator manually as shown in the example below.
const basePath = '/' + bookDef.name; const BookController = defineCrudRestController(BookModel, {basePath}); inject(repoBinding.key)(BookController, undefined, 0); The controller is then attached to the app by calling the app.controller() method.
app.controller(BookController); The new CRUD REST endpoints for the model will be available on the app now.
If you want a customized controller, you can create a copy of defineCrudRestController’s implementation and modify it according to your requirements.
For details about defineCrudRestController and CrudRestControllerOptions, refer to the @loopback/rest-crud API documentation.