Page Contents

Parsing requests

LoopBack 4 uses the Content-Type header and requestBody of the OpenAPI spec to parse the body of http requests. Please see Parsing requests for more details.

The @loopback/rest module ships a set of built-in body parsers:

  • json: parses the http request body as a json value (object, array, string, number, boolean, null)
  • urlencoded: decodes the http request body from ‘application/x-www-form-urlencoded’
  • text: parses the http request body as a string
  • stream: keeps the http request body as a stream without parsing
  • raw: parses the http request body as a Buffer

To support more media types, LoopBack defines extension points to plug in body parsers to parse the request body. LoopBack’s request body parsing capability can be extended in the following ways:

Adding a new parser

To add a new body parser, follow the steps below:

  1. Define a class that implements the BodyParser interface:
/**
 * Interface to be implemented by body parser extensions
 */
export interface BodyParser {
  /**
   * Name of the parser
   */
  name: string | symbol;
  /**
   * Indicate if the given media type is supported
   * @param mediaType - Media type
   */
  supports(mediaType: string): boolean;
  /**
   * Parse the request body
   * @param request - http request
   */
  parse(request: Request): Promise<RequestBody>;
}

A body parser implementation class will be instantiated by the LoopBack runtime within the context and it can leverage dependency injections. For example:

export class JsonBodyParser implements BodyParser {
  name = 'json';
  private jsonParser: BodyParserMiddleware;

  constructor(
    @inject(RestBindings.REQUEST_BODY_PARSER_OPTIONS, {optional: true})
    options: RequestBodyParserOptions = {},
  ) {
    const jsonOptions = getParserOptions('json', options);
    this.jsonParser = json(jsonOptions);
  }
  // ...
}

See the complete code at https://github.com/loopbackio/loopback-next/blob/master/packages/rest/src/body-parsers/body-parser.json.ts.

  1. Bind the body parser class to your REST server/application:

For example,

server.bodyParser(XmlBodyParser);

The bodyParser api binds XmlBodyParser to the context with:

  • key: request.bodyParser.XmlBodyParser
  • tag: request.bodyParser

Please note that newly added body parsers are always invoked before the built-in ones.

Contribute a body parser from a component

A component can add one or more body parsers via its bindings property:

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

export class XmlComponent implements Component {
  bindings = [createBodyParserBinding(XmlBodyParser)];
}

Customize parser options

The request body parser options is bound to RestBindings.REQUEST_BODY_PARSER_OPTIONS. To customize request body parser options, you can simply bind a new value to its key.

Built-in parsers retrieve their own options from the request body parser options. The parser specific properties override common ones. For example, given the following configuration:

{
  limit: '1MB'
  json: {
    strict: false
  },
  text: {
    limit: '2MB'
  },
  /**
   * Validation options for AJV, see https://github.com/epoberezkin/ajv#options
   * This setting is global for all request body parsers and it cannot be
   * overridden inside parser specific properties such as `json` or `text`.
   */
  validation: {nullable: true},
}

The json parser will be created with {limit: '1MB', strict: false} and the text parser with {limit: '2MB'}.

Custom parsers can choose to have its own options from the context by dependency injection, for example:

export class XmlBodyParser implements BodyParser {
  name = 'xml';

  constructor(
    @inject('request.bodyParsers.xml.options', {optional: true})
    options: XmlBodyParserOptions = {},
  ) {
    ...
  }
  // ...
}

Extend AJV with custom keywords and formats

In addition to configure AJV options via the validation property backed by RestBindings.REQUEST_BODY_PARSER_OPTIONS, custom Ajv keywords and formats can also be contributed by bindings.

Contribute a keyword

import {AjvKeyword, RestTags} from '@loopback/rest';

ctx
  .bind<AjvKeyword>('ajv.keywords.smallNumber')
  .to({
    name: 'smallNumber', // name of the keyword
    type: 'number',
    validate: (schema: unknown, data: number) => {
      // The number is smaller than 10
      return data < 10;
    },
  })
  .tag(RestTags.AJV_KEYWORD);

This enables Ajv to use a schema like {type: 'number', smallerNumber: true}.

Contribute a format

import {AjvFormat, RestTags} from '@loopback/rest';
ctx
  .bind<AjvFormat>('ajv.formats.int')
  .to({
    name: 'int', // Name of the format
    type: 'number',
    validate: (data: number) => {
      // The number does not have a decimal point
      return !String(data).includes('.');
    },
  })
  .tag(RestTags.AJV_FORMAT);

Now Ajv can understand a schema like {type: 'number', format: 'int'}.

Replace an existing parser

An existing parser can be replaced by binding a different value to the application context.

class MyJsonBodyParser implements BodyParser {
  // ...
}
app.bodyParser(MyJsonBodyParser, RestBindings.REQUEST_BODY_PARSER_JSON);

Remove an existing parser

An existing parser can be removed from the application by unbinding the corresponding key. For example, the following code removes the built-in JSON body parser.

app.unbind(RestBindings.REQUEST_BODY_PARSER_JSON);