Page Contents

What is a Sequence?

A Sequence is a stateless grouping of Actions that control how a Server 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 Server instances handle requests and responses. The DefaultSequence looks like this:

class DefaultSequence {
  async handle(context: RequestContext) {
    try {
      const route = this.findRoute(context.request);
      const params = await this.parseParams(context.request, route);
      const result = await this.invoke(route, params);
      await this.send(context.response, result);
    } catch (error) {
      await this.reject(context, error);
    }
  }
}

Elements

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

Actions

Actions are JavaScript functions that only accept or return Elements. Since the 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-in Actions:

class MySequence extends DefaultSequence {
  async handle(context: RequestContext) {
    // findRoute() produces an element
    const route = this.findRoute(context.request);
    // parseParams() uses the route element and produces the params element
    const params = await this.parseParams(context.request, route);
    // invoke() uses both the route and params elements to produce the result (OperationRetVal) element
    const result = await this.invoke(route, params);
    // send() uses the result element
    await this.send(context.response, result);
  }
}

Custom Sequences

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

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

import {DefaultSequence, Request, Response} from '@loopback/rest';

class MySequence extends DefaultSequence {
  log(msg: string) {
    console.log(msg);
  }
  async handle(context: RequestContext) {
    this.log('before request');
    await super.handle(context);
    this.log('after request');
  }
}

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

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

const app = new RestApplication();
app.sequence(MySequencce);

app.start();

Advanced topics

Customizing Sequence Actions

There might be scenarios where the default sequence ordering is not something you want to change, but rather the individual actions that the sequence will execute.

To do this, you’ll need to override one or more of the sequence action bindings used 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 a controller to the client making the request.

To do this, we’ll register a custom send action by binding a Provider to the RestBindings.SequenceActions.SEND key.

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

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

custom-send.provider.ts

import {Send, Response} from '@loopback/rest';
import {Provider, BoundValue, inject} from '@loopback/context';
import {
  writeResultToResponse,
  RestBindings,
  ParsedRequest,
} from '@loopback/rest';

// Note: This is an example class; we do not provide this for you.
import {Formatter} from '../utils';

export class CustomSendProvider implements Provider<Send> {
  // In this example, the injection key for formatter is simple
  constructor(
    @inject('utils.formatter') public formatter: Formatter,
    @inject(RestBindings.Http.REQUEST) public request: ParsedRequest,
  ) {}

  value() {
    // Use the lambda syntax to preserve the "this" scope for future calls!
    return (response: Response, result: OperationRetval) => {
      this.action(response, result);
    };
  }
  /**
   * Use the mimeType given in the request's Accept header to convert
   * the response object!
   * @param response The response object used to reply to the  client.
   * @param result The result of the operation carried out by the controller's
   * handling function.
   */
  action(response: Response, result: OperationRetval) {
    if (result) {
      // Currently, the headers interface doesn't allow arbitrary string keys!
      const headers = (this.request.headers as any) || {};
      const header = headers.accept || 'application/json';
      const formattedResult = this.formatter.convertToMimeType(result, header);
      response.setHeader('Content-Type', header);
      response.end(formattedResult);
    } else {
      response.end();
    }
  }
}

Our custom provider will automatically read the Accept header from the request context, and then transform the result object so that it matches the specified MIME type.

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

/src/application.ts

import {RestApplication, RestBindings} from '@loopback/rest';
import {
  RepositoryMixin,
  Class,
  Repository,
  juggler,
} from '@loopback/repository';
import {CustomSendProvider} from './providers/custom-send.provider';
import {Formatter} from './utils';
import {BindingScope} from '@loopback/context';

export class YourApp extends RepositoryMixin(RestApplication) {
  constructor() {
    super();
    // Assume your controller setup and other items are in here as well.
    this.bind('utils.formatter')
      .toClass(Formatter)
      .inScope(BindingScope.SINGLETON);
    this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider);
  }
}

As a result, whenever the send action of the DefaultSequence is called, it will make use of your function instead! You can use this approach to override any of the actions listed under the RestBindings.SequenceActions namespace.

Query string parameters

How to get query string param values.

Parsing Requests

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

Invoking controller methods

  • How to use invoke() in simple and advanced use cases.
  • Explain what happens when you call invoke()
  • Mention caching use case
  • Can I call invoke multiple times?

Writing the response

  • Must call sendResponse() exactly once
  • Streams?

Sending errors

  • try/catch details

Keeping your Sequences

  • Try and use existing actions
  • Implement your own version of built in actions
  • Publish reusable actions to npm