Tip: Missing instructions for your LoopBack 3 use case? Please report a Migration docs issue on GitHub to let us know.
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:
@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:
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)
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:
- configure which endpoints are public
- customize model method, but not the endpoint
- add a new model method and a new endpoint
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.
"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
.
"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
.
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
.
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
.
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:
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
.
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}
.
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;
}
}
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.