What is a Sequence?

A Sequence is a stateless grouping of Actions that control how aServer responds to requests.

The contract of a Sequence is simple: it must produce a response to a request.Creating your own Sequence gives you full control over how your Serverinstances handle requests and responses. The DefaultSequence looks like this:

  1. class DefaultSequence {
  2. async handle(context: RequestContext) {
  3. try {
  4. const route = this.findRoute(context.request);
  5. const params = await this.parseParams(context.request, route);
  6. const result = await this.invoke(route, params);
  7. await this.send(context.response, result);
  8. } catch (error) {
  9. await this.reject(context, error);
  10. }
  11. }
  12. }

Elements

In the example above, route, params, and result are all Elements. Whenbuilding sequences, you use LoopBack Elements to respond to a request:

Actions

Actions are JavaScript functions that only accept or return Elements. Sincethe input of one action (an Element) is the output of another action (Element)you can easily compose them. Below is an example that uses several built-inActions:

  1. class MySequence extends DefaultSequence {
  2. async handle(context: RequestContext) {
  3. // findRoute() produces an element
  4. const route = this.findRoute(context.request);
  5. // parseParams() uses the route element and produces the params element
  6. const params = await this.parseParams(context.request, route);
  7. // invoke() uses both the route and params elements to produce the result (OperationRetVal) element
  8. const result = await this.invoke(route, params);
  9. // send() uses the result element
  10. await this.send(context.response, result);
  11. }
  12. }

Custom Sequences

Most use cases can be accomplished with DefaultSequence or by slightlycustomizing it. When an app is generated by the command lb4 app, a sequencefile extending DefaultSequence at src/sequence.ts is already generated andbound for you so that you can easily customize it.

Here is an example where the application logs out a message before and after arequest is handled:

  1. import {DefaultSequence, Request, Response} from '@loopback/rest';
  2. class MySequence extends DefaultSequence {
  3. log(msg: string) {
  4. console.log(msg);
  5. }
  6. async handle(context: RequestContext) {
  7. this.log('before request');
  8. await super.handle(context);
  9. this.log('after request');
  10. }
  11. }

In order for LoopBack to use your custom sequence, you must register it beforestarting your Application:

  1. import {RestApplication} from '@loopback/rest';
  2. const app = new RestApplication();
  3. app.sequence(MySequencce);
  4. app.start();

Advanced topics

Customizing Sequence Actions

There might be scenarios where the default sequence ordering is not somethingyou want to change, but rather the individual actions that the sequence willexecute.

To do this, you’ll need to override one or more of the sequence action bindingsused by the RestServer, under the RestBindings.SequenceActions constants.

As an example, we’ll implement a custom sequence action to replace the default“send” action. This action is responsible for returning the response from acontroller to the client making the request.

To do this, we’ll register a custom send action by binding aProvider to theRestBindings.SequenceActions.SEND key.

First, let’s create our CustomSendProvider class, which will provide the sendfunction upon injection.

/src/providers/custom-send.provider.ts

custom-send.provider.ts

  1. import {Send, Response} from '@loopback/rest';
  2. import {Provider, BoundValue, inject} from '@loopback/context';
  3. import {writeResultToResponse, RestBindings, Request} from '@loopback/rest';
  4. // Note: This is an example class; we do not provide this for you.
  5. import {Formatter} from '../utils';
  6. export class CustomSendProvider implements Provider<Send> {
  7. // In this example, the injection key for formatter is simple
  8. constructor(
  9. @inject('utils.formatter') public formatter: Formatter,
  10. @inject(RestBindings.Http.REQUEST) public request: Request,
  11. ) {}
  12. value() {
  13. // Use the lambda syntax to preserve the "this" scope for future calls!
  14. return (response: Response, result: OperationRetval) => {
  15. this.action(response, result);
  16. };
  17. }
  18. /**
  19. * Use the mimeType given in the request's Accept header to convert
  20. * the response object!
  21. * @param response - The response object used to reply to the client.
  22. * @param result - The result of the operation carried out by the controller's
  23. * handling function.
  24. */
  25. action(response: Response, result: OperationRetval) {
  26. if (result) {
  27. // Currently, the headers interface doesn't allow arbitrary string keys!
  28. const headers = (this.request.headers as any) || {};
  29. const header = headers.accept || 'application/json';
  30. const formattedResult = this.formatter.convertToMimeType(result, header);
  31. response.setHeader('Content-Type', header);
  32. response.end(formattedResult);
  33. } else {
  34. response.end();
  35. }
  36. }
  37. }

Our custom provider will automatically read the Accept header from the requestcontext, and then transform the result object so that it matches the specifiedMIME type.

Next, in our application class, we’ll inject this provider on theRestBindings.SequenceActions.SEND key.

/src/application.ts

  1. import {RestApplication, RestBindings} from '@loopback/rest';
  2. import {
  3. RepositoryMixin,
  4. Class,
  5. Repository,
  6. juggler,
  7. } from '@loopback/repository';
  8. import {CustomSendProvider} from './providers/custom-send.provider';
  9. import {Formatter} from './utils';
  10. import {BindingScope} from '@loopback/context';
  11. export class YourApp extends RepositoryMixin(RestApplication) {
  12. constructor() {
  13. super();
  14. // Assume your controller setup and other items are in here as well.
  15. this.bind('utils.formatter')
  16. .toClass(Formatter)
  17. .inScope(BindingScope.SINGLETON);
  18. this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider);
  19. }
  20. }

As a result, whenever the send action of theDefaultSequenceis called, it will make use of your function instead! You can use this approachto override any of the actions listed under the RestBindings.SequenceActionsnamespace.

Query string parameters and path parameters

OAI 3.0.x describes the data from a request’s header, query and path in anoperation specification’s parameters property. In a Controller method, such anargument is typically decorated by @param(). We’ve made multiple shortcutsavailable to the @param() decorator in the form of@param.<http_source>.<OAI_primitive_type>. Using this notation, pathparameters can be described as @param.path.string. Here is an example of acontroller method which retrieves a Note model instance by obtaining the idfrom the path object.

  1. @get('/notes/{id}', {
  2. responses: {
  3. '200': {
  4. description: 'Note model instance',
  5. content: {'application/json': {schema: getModelSchemaRef(Note)}},
  6. },
  7. },
  8. })
  9. async findById(@param.path.string('id') id: string): Promise<Note> {
  10. return this.noteRepository.findById(id);
  11. }

You can also specify a parameter which is an object value encoded as a JSONstring or in multiple nested keys. For a JSON string, a sample value would belocation={"lang": 23.414, "lat": -98.1515}. For the same location object, itcan also be represented as location[lang]=23.414&location[lat]=-98.1515. Hereis the equivalent usage for @param.query.object() decorator. It takes in thename of the parameter and an optional schema or reference object for it.

  1. @param.query.object('location', {
  2. type: 'object',
  3. properties: {lat: {type: 'number', format: 'float'}, long: {type: 'number', format: 'float'}},
  4. })

The parameters are retrieved as the result of parseParams Sequence action.Please note that deeply nested properties are not officially supported by OASyet and is tracked byOAI/OpenAPI-Specification#1706.Therefore, our REST API Explorer does not allow users to provide values for suchparameters and unfortunately has no visible indication of that. This problem istracked and discussed inswagger-api/swagger-js#1385.

Parsing Requests

Parsing and validating arguments from the request url, headers, and body. Seepage Parsing requests.

Invoking controller methods

The invoke sequence action simply takes the parsed request parameters from theparseParams action along with non-decorated arguments, calls the correspondingcontroller method or route handler method, and returns the value from it. Thedefault implementation ofinvokeaction calls the handler function for the route with the request specificcontext and the arguments for the function. It is important to note thatcontroller methods use invokeMethod from @loopback/context and can be usedwith global and custom interceptors. SeeInterceptor docs formore details. The request flow for two route flavours is explained below.

For controller methods:

  • A controller instance is instantiated from the context. As part of theinstantiation, constructor and property dependencies are injected. Theappropriate controller method is invoked via the chain of interceptors.
  • Arguments decorated with @param are resolved using data parsed from therequest. Arguments decorated with @inject are resolved from the context.Arguments with no decorators are set to undefined, which is replaced by theargument default value if it’s provided.For route handlers, the handler function is invoked via the chain ofinterceptors. The array of method arguments is constructed using OpenAPI specprovided at handler registration time (either via .api() for full schema or.route() for individual route registration).

Writing the response

Thesendsequence action is responsible for writing the result of the invoke action tothe HTTP response object. The default sequence calls send with (transformed)data. Under the hood, send performs all steps required to send back theresponse, from content-negotiation to serialization of the response body. InExpress, the handler is responsible for setting response status code, headersand writing the response body. In LoopBack, controller methods and routehandlers return data describing the response and it’s the responsibility of theSequence to send that data back to the client. This design makes it easier totransform the response before it is sent.

LoopBack 4 does not yet provide first-class support for streaming responses, seeIssue#2230. As ashort-term workaround, controller methods are allowed to send the responsedirectly, effectively bypassing send action. The default implementation of sendis prepared to handle this casehere.

Handling errors

There are many reasons why the application may not be able to handle an incomingrequest:

  • The requested endpoint (method + URL path) was not found.
  • Parameters provided by the client were not valid.
  • A backend database or a service cannot be reached.
  • The response object cannot be converted to JSON because of cyclicdependencies.
  • A programmer made a mistake and a TypeError is thrown by the runtime.
  • And so on.In the Sequence implementation described above, all errors are handled by asingle catch block at the end of the sequence, using the Sequence Actioncalled reject.

The default implementation of reject does the following steps:

  • Callstrong-error-handler tosend back an HTTP response describing the error.
  • Log the error to stderr if the status code was 5xx (an internal servererror, not a bad request).To prevent the application from leaking sensitive information like filesystempaths and server addresses, the error handler is configured to hide errordetails.

  • For 5xx errors, the output contains only the status code and the status namefrom the HTTP specification. For example:

  1. {
  2. "error": {
  3. "statusCode": 500,
  4. "message": "Internal Server Error"
  5. }
  6. }
  • For 4xx errors, the output contains the full error message (error.message)and the contents of the details property (error.details) thatValidationError typically uses to provide machine-readable details aboutvalidation problems. It also includes error.code to allow a machine-readableerror code to be passed through which could be used, for example, fortranslation.
  1. {
  2. "error": {
  3. "statusCode": 422,
  4. "name": "Unprocessable Entity",
  5. "message": "Missing required fields",
  6. "code": "MISSING_REQUIRED_FIELDS"
  7. }
  8. }

During development and testing, it may be useful to see all error details in theHTTP responsed returned by the server. This behavior can be enabled by enablingthe debug flag in error-handler configuration as shown in the code examplebelow. See strong-error-handlerdocs for a list ofall available options.

  1. app.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true});

An example error message when the debug mode is enabled:

  1. {
  2. "error": {
  3. "statusCode": 500,
  4. "name": "Error",
  5. "message": "ENOENT: no such file or directory, open '/etc/passwords'",
  6. "errno": -2,
  7. "syscall": "open",
  8. "code": "ENOENT",
  9. "path": "/etc/passwords",
  10. "stack": "Error: a test error message\n at Object.openSync (fs.js:434:3)\n at Object.readFileSync (fs.js:339:35)"
  11. }
  12. }

Keeping your Sequences

For most use cases, thedefaultsequence supplied with LoopBack 4 applications is good enough forrequest-response handling pipeline. Check outCustom Sequences on how to extend it and implement customactions.

Working with Express middleware

Under the hood, LoopBack leverages Express frameworkand its concept of middleware. To avoid common pitfalls, it is not possible tomount Express middleware directly on a LoopBack application. Instead, LoopBackprovides and enforces a higher-level structure.

In a typical Express application, there are four kinds of middleware invoked inthe following order:

  • Request-preprocessing middleware likecors orbody-parser.
  • Route handlers handling requests and producing responses.
  • Middleware serving static assets (files).
  • Error handling middleware.In LoopBack, we handle the request in the following steps:

  • The built-in request-preprocessing middleware is invoked.

  • The registered Sequence is started. The default implementation of findRouteand invoke actions will try to match the incoming request against thefollowing resources:
    • Native LoopBack routes (controller methods, route handlers).
    • External Express routes (registered via mountExpressRouter API)
    • Static assets
  • Errors are handled by the Sequence using reject action.Let’s see how different kinds of Express middleware can be mapped to LoopBackconcepts:

Request-preprocessing middleware

At the moment, LoopBack does not provide API for mounting arbitrary middleware,we are discussing this feature in issues#1293 and#2035. Please up-votethem if you are interested in using Express middleware in LoopBack applications.

All applications come with cors enabled,this middleware can be configured via RestServer options - seeCustomize CORS.

While it is not possible to add additional middleware to a LoopBack application,it is possible to mount the entire LoopBack application as component of a parenttop-level Express application where you can add arbitrary middleware as needed.You can find more details about this approach inCreating an Express Application with LoopBack REST API

Route handlers

In Express, a route handler is a middleware function that serves the responseand does not call next(). Handlers can be registered using APIs likeapp.get(), app.post(), but also a more generic app.use().

In LoopBack, we typically use Controllers andRoute handlers to implement request handling logic.

To support interoperability with Express, it is also possible to take an ExpressRouter instance and add it to a LoopBack application as an external router - seeMounting an Express Router. This way itis possible to implement server endpoints using Express APIs.

Static files

LoopBack provides native API for registering static assets as described inServe static files. Under the hood, staticassets are served by serve-staticmiddleware from Express.

The main difference between LoopBack and vanilla Express applications: LoopBackensures that static-asset middleware is always invoked as the last one, onlywhen no other route handled the request. This is important for performancereasons to avoid costly filesystem calls.

Error handling middleware

In Express, errors are handled by a special form of middleware, one that’saccepting four arguments: err, request, response, next. It’s up to theapplication developer to ensure that error handler is registered as the lastmiddleware in the chain, otherwise not all errors may be routed to it.

In LoopBack, we use async functions instead of callbacks and thus can use simpletry/catch flow to receive both sync and async errors from individualsequence actions. A typical Sequence implementation then passes these errors tothe Sequence action reject.

You can learn more about error handling inHandling errors.