Page Contents

Introduction

This document will guide you in migrating custom model methods in LoopBack 3 to their equivalent implementations in LoopBack 4.

In LoopBack 3, a model class has three responsibilities: a model describing shape of data, a repository providing data-access APIs, and a controller implementing REST API.

In LoopBack 4,

  • data-access APIs are implemented by repositories that are decoupled from models.
  • REST APIs are implemented by controllers that are decoupled from models.

A Repository represents a specialized service interface that provides strong-typed data access (for example, CRUD) operations of a domain model against the underlying database or service. A single model can be used with multiple different repositories. A Repository can be defined and implemented by application developers. LoopBack ships a few predefined repository interfaces for typical CRUD and KV operations. These repository implementations leverage model definition and dataSource configuration to fulfill the logic for data access. See more examples at Repository/CrudRepository/EntityRepository and KeyValueRepository.

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. A Controller operates only on processed input and abstractions of backend services / databases.

Before we discuss any customizations and how to migrate them, let’s first go over some LoopBack 4 basics with regards to models, repositories, and controllers.

A CLI-generated model named Note would look like this:

src/models/note.model.ts

@model()
export class Note extends Entity {
  @property({
    type: 'number',
    id: true,
    generated: true,
  })
  id?: number;

  @property({
    type: 'string',
    required: true,
  })
  title: string;

  @property({
    type: 'string',
  })
  content?: string;

  constructor(data?: Partial<Note>) {
    super(data);
  }
}

export interface NoteRelations {
  // describe navigational properties here
}

export type NoteWithRelations = Note & NoteRelations;

A model describes business domain objects, for example, Customer, Address, and Order. It usually defines a list of properties with name, type, and other constraints like relations.

A CLI-generated repository for a model Note would look like this:

src/repositories/note.repository.ts

export class NoteRepository extends DefaultCrudRepository<
  Note,
  typeof Note.prototype.id,
  NoteRelations
> {
  constructor(@inject('datasources.db') dataSource: DbDataSource) {
    super(Note, dataSource);
  }
}

It extends DefaultCrudRepository . It handles all persistence operations against the datasource.

A CLI-generated controller for the model Note would look like this: (To save space, only a partial implementation is shown)

src/controllers/note.controller.ts

export class NoteController {
  constructor(
    @repository(NoteRepository)
    public noteRepository: NoteRepository,
  ) {}

  @post('/notes', {
    responses: {
      '200': {
        description: 'Note model instance',
        content: {'application/json': {schema: getModelSchemaRef(Note)}},
      },
    },
  })
  async create(
    @requestBody({
      content: {
        'application/json': {
          schema: getModelSchemaRef(Note, {
            title: 'NewNote',
            exclude: ['id'],
          }),
        },
      },
    })
    note: Omit<Note, 'id'>,
  ): Promise<Note> {
    return this.noteRepository.create(note);
  }

  // ...
  // remaining CRUD endpoints
  // ...
}

For a full example of a CLI-generated controller for a model Todo, see TodoController .

One of the first things you will notice in the controller is that it injects the repository NoteRepository to handle all persistence operations.

Looking at NoteController’s create method, you will notice that it is decorated with a @post decorator to indicate that it handles POST requests. It also defines the endpoint url '/notes' and its OpenAPI operation specification. The controller’s create method relies on the NoteRepository’s create method to handle the actual persistence operation.

The remaining controller methods are set up in a similar way.

LoopBack 3 Approaches to Customizing Model Methods

In LoopBack 3, a developer is able to customize model methods in various ways:

In the sections below, we will show some examples of these, and how to implement them in LoopBack 4.

Configure Which Endpoints are public

By default, LoopBack 3 and 4 automatically makes each endpoint public. Steps must be taken in LoopBack 3 and 4 to prevent an endpoint from being public.

LoopBack 3 Approach

In LoopBack 3 LoopBack models automatically have a standard set of HTTP endpoints that provide REST APIs for create, read, update, and delete (CRUD) operations on model data.

To control whether the REST API from is public or not, the developer has two options:

  • specifying certain options in server/model-config.json
  • calling disableRemoteMethodByName() from model’s script file

See Exposing and hiding models, methods, and endpoints for details.

/server/model-config.json

"Note": {
  "public": true,
  "dataSource": "db"
},

This exposes all the REST API endpoints of the model. The default for public is true, so it is not necessary to add this property at all. But doing so makes it very clear. To hide all the REST API endpoints of the model, simply change public to false.

/server/model-config.json

"MyUser": {
  "public": true,
  "dataSource": "db",
  "options": {
    "remoting": {
      "sharedMethods": {
        "*": false,
        "login": true,
        "logout": true
      }
    }
  }
}

This hides all the REST API endpoints of the model, except login and logout.

common/models/Note.js

Note.disableRemoteMethodByName('create');
Note.disableRemoteMethodByName('upsert');
Note.disableRemoteMethodByName('deleteById');
...

This disables specific REST API endpoints of the model by name.

LoopBack 4 Approach

As mentioned earlier, REST API endpoints are implemented by a controller class. Preventing an endpoint from being public is simple.

To hide a particular REST API endpoint, simply delete the appropriate function and its decorator from the controller class.

To hide all REST API endpoints of a particular model, simply do not create a controller class for it.

Customize Model Method But Not the Endpoint

LoopBack 3 and 4 provide a lot of out-of-the-box functionality for developers with regards to data-access APIs and REST APIs. Only a few steps are required to create minor customizations.

LoopBack 3 Approach

To override the behaviour of a PersistedModel the developer has two options for writing custom methods:

The two approaches above are similar, so, for brevity, we will only discuss the model script approach in the examples below.

The following LB3 code snippet shows how to customize the built-in find() method for the model Note.

common/models/Note.js

module.exports = function (Note) {
  Note.on('dataSourceAttached', function (obj) {
    var find = Note.find;
    var cache = {};

    Note.find = function (filter, options, cb) {
      var key = '';
      if (filter) {
        key = JSON.stringify(filter);
      }
      var cachedResults = cache[key];
      if (cachedResults) {
        console.log('serving from cache');
        process.nextTick(function () {
          cb(null, cachedResults);
        });
      } else {
        console.log('serving from db');
        find.call(Note, function (err, results) {
          if (!err) {
            cache[key] = results;
          }
          cb(err, results);
        });
      }
    };
  });
};

It basically builds up a cache to limit database queries. No changes are made to any REST API endpoint settings.

LoopBack 4 Approach

As mentioned earlier, in LoopBack 4, data-access APIs are implemented by repositories. The repository for your model usually extends a default implementation. So to customize the behaviour of a particular method, you simply override this method in your repository class.

The following LB4 code snippets show how to customize the find method of NoteRepository.

src/repositories/note.repository.ts

export class NoteRepository extends DefaultCrudRepository<
  Note,
  typeof Note.prototype.id,
  NoteRelations
> {
  constructor(
    @inject('datasources.db') dataSource: DbDataSource,
    @inject('my.cache') private cache: Map<string, Note[]>,
  ) {
    super(Note, dataSource);
  }

  async find(
    filter?: Filter<Note>,
    options?: Options,
  ): Promise<(Note & NoteRelations)[]> {
    let key: string = '';
    if (filter) {
      key = JSON.stringify(filter);
    }

    let cachedResults: Note[] | undefined = this.cache.get(key);

    if (cachedResults) {
      console.log('serving from cache');
      return cachedResults;
    } else {
      console.log('serving from db');

      let results: Note[] | undefined = await super.find(filter, options);
      this.cache.set(key, results);

      return results;
    }
  }
}

The cache is injected into the repository.

It was defined in the application class:

src/application.ts

export class NoteApplication extends BootMixin(
  ServiceMixin(RepositoryMixin(RestApplication)),
) {
  private cache: Map<string, Note[]> = new Map();

  constructor(options: ApplicationConfig = {}) {
    super(options);

    this.bind('my.cache').to(this.cache);

    // ...
  }
}

No changes are made to any REST API endpoint settings in NoteController.

Add a New Model Method And a New Endpoint

LoopBack 3 and 4 provide a lot of out-of-the-box functionality for developers with regards to data-access APIs and REST APIs. Only a few steps are required to create minor additions.

LoopBack 3 Approach

The following LB3 code snippet shows how to customize the Note model by adding a new persisted model method findByTitle, and defining a new remote method for its respective endpoint /findByTitle. The full endpoint url will end up being /Notes/findByTitle.

common/models/Note.js

module.exports = function (Note) {
  Note.remoteMethod('findByTitle', {
    http: {
      path: '/findByTitle',
      verb: 'get',
    },
    accepts: {arg: 'title', type: 'string'},
    returns: {arg: 'note', type: [Note], root: true},
  });

  Note.findByTitle = function (title, cb) {
    var titleFilter = {
      where: {
        title: title,
      },
    };
    Note.find(titleFilter, function (err, foundNotes) {
      if (err) {
        cb(err);
      } else {
        cb(null, foundNotes);
      }
    });
  };
};

LoopBack 4 Approach

To accomplish the same thing in LoopBack 4, we can add a new method findByTitle to NoteRepository, a new method findByTitle to NoteController, and specify an endpoint url of /notes/byTitle/{title}.

src/repositories/note.repository.ts

export class NoteRepository extends DefaultCrudRepository<
  Note,
  typeof Note.prototype.id,
  NoteRelations
> {
  constructor(@inject('datasources.db') dataSource: DbDataSource) {
    super(Note, dataSource);
  }

  async findByTitle(title: string): Promise<Note[]> {
    const titleFilter = {
      where: {
        title: title,
      },
    };
    const foundNotes = await this.find(titleFilter);
    return foundNotes;
  }
}

src/controllers/note.controller.ts

export class NoteController {
  constructor(
    @repository(NoteRepository)
    public noteRepository: NoteRepository,
  ) {}

  @get('/notes/byTitle/{title}', {
    responses: {
      '200': {
        description: 'Array of Note model instances',
        content: {
          'application/json': {
            schema: {
              type: 'array',
              items: getModelSchemaRef(Note, {includeRelations: true}),
            },
          },
        },
      },
    },
  })
  async findByTitle(
    @param.path.string('title') title: string,
  ): Promise<Note[]> {
    return this.noteRepository.findByTitle(title);
  }

  // ...
  // remaining CRUD endpoints
  // ...
}

Summary

As the examples above show, migrating custom model methods from LoopBack 3 to LoopBack 4 is relatively straightforward.