LoopBack 4 Tutorial
As one of the core developers of LoopBack, a very frequent complaint I hear is about our "not-so-great" documentation.
While we are actively addressing it and have made documentation improvement as one of our highest priorities this year, I am taking up "better learning materials for LoopBack" as a personal goal and plan to completely change the perception of LoopBack. This post is the first among many that I will be writing on LoopBack.
In my opinion, a semi-informal presentation is the best format for teaching. However, official documentation follow certain guidelines and generally don't allow for such an approach. On my website I am free from such guidelines, which allow me to write write tutorials the way I think they should be structured and written.
Here's my take on the LoopBack 4 tutorial. Let's begin!
Understanding LoopBack 4#
There are two things you must know right at the beginning.
- LoopBack 4 as an entity does not exist.
- LoopBack is not a REST framework.
"What! なに!"
Let me explain.
There is no LoopBack 4 on NPM, like how there is LoopBack 3 (loopback), and there is nothing that can be downloaded that would constitute as LoopBack 4. The closest LoopBack 4 comes to being a tangible entity is the loopback-next monorepo, and maybe the lb4 CLI.
lb4
CLI is a great tool, it gives the wrong impression that it is LoopBack 4 and that it is a REST framework. I am suggesting to change it.LoopBack 4 is a set of libraries from the @loopback
organization on NPM that you assemble together to create apps and frameworks. These libraries can have new major, minor, and patch versions, but there's never gonna be LoopBack 4.2.1 etc.
LoopBack 4 is a lot like you, with your organs being the libraries. What composition of your organs become you and when do does the composition cease to be you?
As of today, if you came here looking for "LoopBack 4 tutorial", you most probably meant "LoopBack 4 REST framework tutorial".
Although, LoopBack is not a REST framework, it provides all the libraries that can be put together to create a REST framework. Let's assemble a REST framework and build an app on top of it.
First step: create a directory named best
and initialize it as an NPM package.
$ mkdir best
$ cd best
$ npm init
Then install typescript
as a development dependency.
$ npm i -D typescript
We will be using ts-node for executing TypeScript files, so install it too as a development dependency.
$ npm i -D ts-node
Add a start
script on the package.json
file.
"start": "ts-node ."
This will help us to start the app using the familiar npm start
command rather than having to type ts-node .
.
Add a tsconfig.json
file in the directory with the following content.
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"incremental": true,
"lib": ["es2020"],
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"target": "es2018",
"sourceMap": true,
"declaration": true,
"importHelpers": true
}
}
From this point on let's refer to LoopBack 4 as simply "LoopBack".
RestApplication#
The @loopback/rest package exports classes and utilities for creating REST applications. RestApplication
is a class exported by this package; from the name of the class it is obvious, it is what we should be using for creating REST applications.
RestApplication
is an implementation of Application
(from @loopback/core
) especially customized for working with REST requirements.Install @loopback/rest
as a dependency in the application directory.
$ npm i @loopback/rest
Let's see what RestApplication
does. Create a file named index.ts
with the following content.
import { RestApplication } from '@loopback/rest';
async function main() {
const app = new RestApplication();
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
Then start the app.
$ npm start
> best@1.0.0 start /Users/yaapa/projects/best
> ts-node .
Server running at http://[::1]:3000
const app = new RestApplication({
rest: {
port: 3001,
host: '127.0.0.1'
}
});
For more application configuration options refer to RestApplication Options.
When you visit http://[::1]:3000, you will be greeted with 404 Endpoint "GET /" not found.
Initially it may come as a surpirse to you that you got a 404
, but when you remind yourself that you are creating a REST application it shouldn't be surprising that you got a 404
from a system that has nothing.
"So, how do we add REST endpoints to this application?"
There is a software design pattern called Model-View-Controller (MVC), which divides the functionality of the system into three losely coupled logical components.
- Model - stores the data
- View - composes query results
- Controller - interface for querying the data
In a MVC web framework, controllers are what create endpoints. LoopBack is a MVC framework, the RestApplication
class provides a .controller()
method for adding controllers.
Controllers#
LoopBack controllers are standard TypeScript classes decorated with OpenAPI Decorators; the most basic of which are the operation decorators, these decorators are named @get
, @post
, @del
etc., and create endpoints for the corresponding HTTP methods. The return value of the decorated method is the response value of the request.
Let's begin by adding a very simple controller, which will create an endpoint at /hi
for GET requests and responds with a "Hi!".
Update index.ts
to the following.
import { RestApplication, get } from '@loopback/rest';
class MyController {
@get('/hi')
greet() {
return 'Hi!';
}
}
async function main() {
const app = new RestApplication();
app.controller(MyController);
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
Restart the app and make a request to http://[::1]:3000/hi, you see "Hi!" in the response.
A question that may come to your mind.
"Can I access the response object and send the response myself?"
Yes, you can.
Just inject the response object (accessed using the RestBindings.Http.RESPONSE
key from the context) into the controller class as a property.
You will need the @loopback/core
package for that, so install it as a dependency.
$ npm i @loopback/core
The objects in the Context are referred to, using using their binding keys. For a list of default binding keys refer to Reserved Binding Keys.
Conventionally, to pass a dependency to a class or a function, the code initializing the class or calling the function have access to the dependency and pass it along during the invocation. When using dependency injection, the class or function does not depend on the caller for the dependency, it decides which dependency to it needs and "acquires" it from the DI container; it depends on the caller only for the invocation. This pattern is called Inversion of Control (IoC).
If @inject()
sounds confusing, think of it as @acquire()
, you will have a better idea of what's going on.
The index.ts
file should look something like this now.
import { RestApplication, get, RestBindings, Response } from '@loopback/rest';
import {inject} from '@loopback/core';
class MyController {
// Response object added via dependency injection using @inject() decorator
constructor(@inject(RestBindings.Http.RESPONSE) private res: Response) {}
@get('/hi')
greet() {
this.res.send('Hello!');
}
}
...
As you might have guessed, the request object or any other object from the Context can also be injected, provided you know the binding key.
import { RestApplication, get, RestBindings, Response, Request } from '@loopback/rest';
import {inject} from '@loopback/core';
class MyController {
// Request and response objects added via dependency injection using @inject() decorator
constructor(
@inject(RestBindings.Http.REQUEST) private req: Request,
@inject(RestBindings.Http.RESPONSE) private res: Response
) {}
@get('/hi')
greet() {
this.res.send(`Hello ${this.req.ip}!`);
}
}
...
We have gotten our app to send some responses now, but what's the big deal about it? Even Express can do that, right?
Indeed, if we wre to use LoopBack for writing APIs that way, we might as well use Express. But that's not how we create APIs in LoopBack, we use OpenAPI Specification.
OpenAPI Specification (OAS)#
OAS is a standard for defining REST APIs in a structure that humans and machines can understand. It describes all the available API endpoints, along with the request parameters, and the response object - in JSON or YAML format.
Since all the API details are clearly defined, it is the API documentation too.
"What does OAS look like?"
Here is a simple example.
openapi: 3.0.0
info:
title: Best Application
version: 2.0.0
paths:
"/greet":
get:
parameters:
- name: name
in: query
schema:
type: string
responses:
'200':
description: Greets with a name
content:
application/json:
schema:
type: object
properties:
name:
type: string
number:
type: number
"/":
get:
responses:
'200':
description: Home GET
content:
application/json:
schema:
type: string
post:
responses:
'200':
description: Home POST
content:
application/json:
schema:
type: number
Focus on the "paths" propery. You know that:
- There are two API endpoints - /greet and /
- /greet accepts GET requests with a parameter named
name
in the query string, and returns an object withname
(string) andnumber
(number) properties as the response. - / accepts GET and POST requests. On receiving a GET request it returns a string as a response. On receiving a POST request it returns a number as a response.
"OK. Now how do I implement this specification in my app?"
Use the .api()
method of the RestApplication
class to pass an OpenApiSpec
object along with the operation functions for the endpoints in the x-operation
property as shown in the following example.
Scroll past the code block if it is starting to sound confusing.
...
import { OpenApiSpec, ... } from '@loopback/rest';
...
const spec: OpenApiSpec = {
openapi: '3.0.0',
info: {
title: 'Best Application',
version: '2.0.0',
},
paths: {
'/': {
get: {
'x-operation': function() {
return 'Welcome!';
},
responses: {
'200': {
description: 'Home GET',
content: {
'application/json': {
schema: {type: 'string'},
},
},
},
},
},
post: {
'x-operation': function() {
return 100;
},
responses: {
'200': {
description: 'Home POST',
content: {
'application/json': {
schema: {type: 'number'},
},
},
},
},
},
},
'/greet': {
get: {
'x-operation': function(name: string) {
return {
name,
luckyNumber: Math.floor(Math.random() * 10)
}
},
parameters: [{name: 'name', in: 'query', schema: {type: 'string'}}],
responses: {
'200': {
description: 'Greets with a name',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: {type: 'string'},
number: {type: 'number'}
}
},
},
},
},
},
},
},
},
};
...
app.api(spec);
...
"Will I have to manually write the OAS for my app? Looks like an utterly tedious and error-prone task. Who wants to do that?"
LoopBack will do it for you - IF - you create OAS-decorated controllers. Remember @get
, @post
, etc.?
Revisiting controllers#
This is our index.ts
file as of now.
import { RestApplication, get } from '@loopback/rest';
class MyController {
@get('/hi')
greet() {
return 'Hi!';
}
}
async function main() {
const app = new RestApplication();
app.controller(MyController);
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
Usually, each model has a corresponding controller, and a controller can have multiple methods.
Imagine having all those controllers and their methods defined in this file - it would be a noisy, unmanageable mess - something we definitely don't want.
"How about we create a directory named controllers
, keep the controller files there and import them into the index.ts
file?"
How about we create a directory named controllers
, keep the controller files there and let LoopBack load all of them for us, instead?
"Is that possible?"
Yes.
To enable that, we will have to compose BootMixin
(from @loopback/boot
) onto RestApplication
. It will add the functionality to discover and boot LoopBack artifacts like controllers in our app.
However, there is a bit of a problem - BootMixin
works with transpiled JavaScript files, not TypeScript files. That means we will have to ditch ts-node
in favor of a real transpiler that will generate JavaScript files from our TypeScript source code.
There is another thing - with BootMixin
in picture, we cannot directly instantiate an instance of RestApplication
anymore. But that's a good thing, our app which extends RestApplication
, can be now customized at class level.
BootMixin
resides in the @loopback/boot
package, install it.
$ npm i @loopback/boot
We will be using lb-tsc
for transpiling our TypeScript files, install its package @loopback/build
as a development dependency.
$ npm i -D @loopback/build
Update the tsconfig.json
for the new changes.
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"incremental": true,
"lib": ["es2020"],
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"target": "es2018",
"sourceMap": true,
"declaration": true,
"importHelpers": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
As evident from the changes in the tsconfig.json
file, we will be keeping all our TypeScript files in the src
directory. And with BootMixin
in picture, our app will extend RestApplication
.
Extending RestApplication#
Here is the new index.ts
file in the src
directory.
import { RestApplication } from '@loopback/rest';
import { BootMixin } from '@loopback/boot';
class BestApplication extends BootMixin(RestApplication) {
constructor() {
super();
this.projectRoot = __dirname;
}
}
async function main() {
const app = new BestApplication();
await app.boot();
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
Next, create a directory named controllers
in the src
directory for our controllers.
Create a file named greet.controller.ts
with the following content in the src/controllers
directory.
import { get, param } from '@loopback/rest';
type Greeting = {
name: string
luckyNumber: number
};
export class GreetController {
constructor() {}
@get('/greet', {
responses: {
'200': {
description: 'Greets with a name',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: {type: 'string'},
number: {type: 'number'}
}
}
}
}
}
}
})
greet(@param.query.string('name') name: string): Greeting {
return {
name: name,
luckyNumber: Math.floor(Math.random() * 10)
};
}
}
Notice how the operation function greet
is returning an object as a response.
Then create an index.ts
file for controllers, which will export all the files in the src/controllers
directory.
export * from './greet.controller';
Make sure to update this file when you add any new controller files.
The changes we have done so far will require us to make some changes in the package.json
file too.
{
"name": "best",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "lb-tsc",
"prestart": "npm run build",
"start": "node -r source-map-support/register ."
},
"dependencies": {
"@loopback/boot": "^2.4.1",
"@loopback/core": "^2.9.4",
"@loopback/rest": "^6.0.0"
},
"devDependencies": {
"@loopback/build": "^6.2.0",
"typescript": "^3.9.7"
}
}
We have removed ts-node
from development dependencies.
Now when you run npm start
, lb-tsc
will transpile the TypeScript files in the build step, then node
will execute the dist/index.js
file. Give it a try.
$ npm start
> best@1.0.0 prestart /Users/yaapa/projects/best
> npm run build
> best@1.0.0 build /Users/yaapa/projects/best
> lb-tsc
> best@1.0.0 start /Users/yaapa/projects/best
> node -r source-map-support/register .
Server running at http://[::1]:3000
LoopBack will have created the http://[::1]:3000/greet endpoint from the controller, load it in a browser for confirmation. Pass a name
parameter to the endpoint and see what happens.
LoopBack will also have generated the OAS for your app. Load http://[::1]:3000/openapi.json in the browser to see it.
{
"openapi": "3.0.0",
"info": {
"title": "best",
"version": "1.0.0",
"contact": {}
},
"paths": {
"/greet": {
"get": {
"x-controller-name": "AuthorController",
"x-operation-name": "greet",
"tags": [
"AuthorController"
],
"responses": {
"200": {
"description": "Greets with a name",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"number": {
"type": "number"
}
}
}
}
}
}
},
"parameters": [
{
"name": "name",
"in": "query",
"schema": {
"type": "string"
}
}
],
"operationId": "AuthorController.greet"
}
}
},
"servers": [
{
"url": "/"
}
]
}
It's cool to see the result of the changes we made, but that's now how real-world REST API controllers work. API responses are based on models and results are retrieved from databases. Let's add a model to our app.
Models#
LoopBack models are TypeScript classes decorated with the @model
decorator and properties decorated with the @property
decorator.
They look something like this.
import { Entity, model, property } from '@loopback/repository';
@model()
export class Author extends Entity {
@property({
type: 'number',
id: true,
generated: true,
})
id?: number;
@property({
type: 'string',
required: true,
})
name: string;
constructor(data?: Partial<Author>) {
super(data);
}
}
export interface AuthorRelations {}
export type AuthorWithRelations = Author & AuthorRelations;
LoopBack models only define the shape of the data, they don't connect to the database or query it.
"So how do we save data or query the database?"
We will get there in a while, first let's set up the models
directory for our app.
Create a directory named models
and save the file above as author.model.ts
.
Then create an index.ts
file, which will export all the models from the directory.
export * from './author.model';
Make sure to update this file when if you add any new model files in the models
directory.
As you might have guessed, LoopBack will discover and boot the model files too. Yes, that will happen provided we compose our app with RepositoryMixin
.
The src/index.ts
file should look like this now.
import { RestApplication } from '@loopback/rest';
import { BootMixin } from '@loopback/boot';
import { RepositoryMixin } from '@loopback/repository';
class BestApplication extends BootMixin(RepositoryMixin(RestApplication)) {
constructor() {
super();
this.projectRoot = __dirname;
}
}
async function main() {
const app = new BestApplication();
await app.boot();
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
@loopback/repository
is now a dependency of the app, install it.
$ npm i @loopback/repository
What is "repository"? Is it related to Git repository?
Repository is a data access pattern; it is an abstraction for managing model relations and database queries. Remember LoopBack models don't connect to databases or query them? It is the repository which does it. The LoopBack repository artifact is not related to Git or GitHub.
If that is the case, couldn't you all come up with a less confusing name?
We did not invent it. It is older than Git. Learn more about the Repository Pattern here.
Repositories#
A repository is the relations and query manager of a model. It is the layer where model relations like hasMany, belongsTo etc., are created and queries made to the database. Although, you can directly call repository methods, in a typical LoopBack application, it is the controller that calls the repository methods.
With repositories playing such a crucial role, you can think of LoopBack as a Model-Repository-View-Controller (MRVC) framework.
Although Repositories provide the methods for making queries, it depend on another artifact for connectivity and the actual query - connector.
Connectors#
A connector is a LoopBack database driver. All connectors expose a standard connectivity and query API. For querying the database, users invoke connector methods instead of writing database-specific queries. Since all connectors expose the same API, switching the database is just a matter of switching the connectors - no need for re-writing queries, or writing them at all.
LoopBack has official connectors for: In-memory db, In-memory key-value, MySQL, MongoDB, Redis, Cassandra, gRPC, PostgreSQL, OracleDB, IBM Object Storage, IBM Db2 (for Linux, Unix, Windows), IBM Db2 for i, IBM Db2 for z/OS, IBM DashDB, IBM MQ Light, IBM Cloudant DB, Couchdb 2.x, IBM WebSphere eXtreme Scale key-value, Microsoft SQL, and z/OS Connect Enterprise Edition.
"Why create your own drivers? Why not use the official ones?"
In most cases, we use the official drivers under the hood, we just provide a uniformity wrapper on top of them.
Directly using a connector with LoopBack is possible but is not very fexlible, therefore we use datasources as the flexibility glue.
Datasources#
A datasource is a named configuration for a connection to a database. It stores the connectivity and the connector details.
"So, what's the difference between a connector and a datasource?"
A LoopBack connector is a database or service specific library exposing the standard LoopBack connector API. A datasource is the configuration for this connector, which makes it usable with LoopBack.
"Can we see how repositories, connectors and datasources look like?"
As a LoopBack user, you will not be interacting with connectors directly, so let's keep that for another day. But you will certainly get to see how repositories and datasources look like.
Manually creating artifacts like model, datasources, repositories, and controllers are very tedious, so we will use the lb4
command-line tool to generate them from this point on.
The lb4
CLI#
The lb4
CLI is a tool for generating LoopBack artifacts. Install it.
$ npm i -g @loopback/cli
Generating models#
Use the lb4 model
command to generate models. We already have a manually created Author
model, let's create a Book
model.
$ lb4 model Book
? Please select the model base class Entity (A persisted model with an ID)
? Allow additional (free-form) properties? No
Model Book will be created in src/models/book.model.ts
Let's add a property to Book
Enter an empty property name when done
? Enter the property name: id
? Property type: number
? Is id the ID property? Yes
? Is id generated automatically? Yes
Let's add another property to Book
Enter an empty property name when done
? Enter the property name: title
? Property type: string
? Is it required?: Yes
Let's add another property to Book
Enter an empty property name when done
? Enter the property name:
create src/models/book.model.ts
update src/models/index.ts
Model Book was/were created in src/models
Generating datasources#
Before we can store our model data, we need a datasource. So let's create one. We don't want to get into the complexities of setting up a database, so we will use "In-memory db" - it stores the data in the application memory.
Use the lb4 datasource
command to create a datasource. We'll go with the name memory
.
$ lb4 datasource memory
? Select the connector for memory: In-memory db (supported by StrongLoop)
? window.localStorage key to use for persistence (browser only):
? Full path to file for persistence (server only):
create src/datasources/memory.datasource.ts
update src/datasources/index.ts
Datasource Memory was/were created in src/datasources
Generating Repositories#
With a datasource in place, we can now create repositories for our models using the lb4 repository
command.
Unlike the previous two commands, the lb4 repository
command is not limited to one artifact at a time - it creates individual repositories for all the selected models. So when prompted with "Select the model(s) you want to generate a repository for", press a without any fear or doubt.
$ lb4 repository
? Please select the datasource MemoryDatasource
? Select the model(s) you want to generate a repository for Author, Book
? Please select the repository base class DefaultCrudRepository (Juggler bridge)
create src/repositories/author.repository.ts
create src/repositories/book.repository.ts
update src/repositories/index.ts
update src/repositories/index.ts
Repositories AuthorRepository, BookRepository was/were created in src/repositories
Generating Controllers#
And now, time to create the controllers for our models.
Use the lb4 controller
command to generate a controller for the Author
model.
$ lb4 controller Author
Controller Author will be created in src/controllers/author.controller.ts
? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? Author
? What is the name of your CRUD repository? AuthorRepository
? What is the name of ID property? id
? What is the type of your ID? number
? Is the id omitted when creating a new instance? Yes
? What is the base HTTP path name of the CRUD operations? /authors
create src/controllers/author.controller.ts
update src/controllers/index.ts
Controller Author was/were created in src/controllers
Similarlity, generate the Book
controller.
$ lb4 controller Book
Controller Book will be created in src/controllers/book.controller.ts
? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? Book
? What is the name of your CRUD repository? BookRepository
? What is the name of ID property? id
? What is the type of your ID? number
? Is the id omitted when creating a new instance? Yes
? What is the base HTTP path name of the CRUD operations? /books
create src/controllers/book.controller.ts
update src/controllers/index.ts
Controller Book was/were created in src/controller
With the new artifacts wired up, the app is ready to expose a "real" REST API, not a hand-crafted one.
lb4
CLI can scaffold a whole project too, not just generate models, datasources, repositories, and controllers. It is recommended to use lb4
to scaffold your app instead of manually creating the index and other files as shown in this tutorial.API Explorer#
The LoopBack API Explorer is a GUI for listing the API endpoints of a OAS-compliant application and interacting with it; it is based on the Swagger UI.
Restart the app and go to http://[::1]:3000/explorer, it will load the API Explorer.
Play around with your REST API!
The UI of the API Explorer is rendered based on the OAS file at http://[::1]:3000/openapi.json. So, if something's not right in the UI, the cause is somewhere in the process that generated the openapi.json
file. If you want to see some change in the UI, the change will, mostly probably have to be made in the model or controller files.
In real-world apps, models are often related, they don't exist as standalone enities. How do we create model relations in LoopBack?
Model relations#
The lb4
CLI comes with a command (relation
) for creating relations between models.
We have a Author
model and a Book
model - a perfect combination for creating relations.
Books belong to authors, so let's create a BelongsTo relation from Book
to Author
.
$ lb4 relation
? Please select the relation type belongsTo
? Please select source model Book
? Please select target model Author
? Foreign key name to define on the source model authorId
? Relation name author
? Allow Book queries to include data from related Author instances? Yes
create src/controllers/book-author.controller.ts
Relation BelongsTo was/were created in src
The step above makes the following changes:
- Adds a foreign key named
authorId
to theBook
model, decorated with the@belongsTo()
decorator; take a look at thebook.model.ts
file. - Adds the implementation details of the BelongsTo relation in the
book.repository.ts
file. - Creates a new controller file,
book-author.controller.ts
.
By specifying the authorId
, now you will be able to specify which Author
a book belongs to when you create a new Book
. And you can get the author of a book at GET /books/{id}/author
.
Book
with non-existing authorId
s. LoopBack does not check for the existence of models with the specified foreign keys (doing so will greatly reduce the speed of creating new instances); it is imposed by the underlying database's support for referential integrity or the lack of it. If you switch to a SQL database, you will encounter an error if you try to create an instance of Book
with a non-existing authorId
.Now, let's create a HasMany relation from Author
to Book
,
lb4 relation
? Please select the relation type hasMany
? Please select source model Author
? Please select target model Book
? Foreign key name to define on the target model authorId
? Source property name for the relation getter (will be the relation name) books
? Allow Author queries to include data from related Book instances? Yes
create src/controllers/author-book.controller.ts
Relation HasMany was/were created in src
The step above makes the following changes:
- Adds a new property named
books
to theAuthor
model, decorated with the@hasMany()
decorator; take a look at theauthor.model.ts
file. - Adds the implementation details of the HasMany relation in the
author.repository.ts
file. - Creates a new controller file,
author-book.controller.ts
.
Now you have API endpoints at /authors/{id}/books
for creating books for a particular author, refered to by its id.
LoopBack supports more relation types, refer to them at https://loopback.io/doc/en/lb4/Relations.html.
Local API Explorer#
If you didn't notice aleady, the URL of the API Explorer in the browser is http://explorer.loopback.io?url=http://[::1]:3000/openapi.json, not http://[::1]:3000/explorer.
"So what happened here?"
The request to http://[::1]:3000/explorer was redirected to a remotely hosted instance of API Explorer at http://explorer.loopback.io.
"Is there a way to host my own API Explorer locally?"
Yes, there is. Use the RestExplorerComponent
component.
Install it.
$ npm i @loopback/rest-explorer
Then load it in your app; update the index.ts
file as shown below. Restart the app.
import { RestApplication } from '@loopback/rest';
import { BootMixin } from '@loopback/boot';
import { RepositoryMixin } from '@loopback/repository';
import {
RestExplorerBindings,
RestExplorerComponent,
} from '@loopback/rest-explorer';
class BestApplication extends BootMixin(RepositoryMixin(RestApplication)) {
constructor() {
super();
this.projectRoot = __dirname;
// Configure REST Explorer options
this.configure(RestExplorerBindings.COMPONENT).to({
path: '/explorer',
});
this.component(RestExplorerComponent);
}
}
async function main() {
const app = new BestApplication();
await app.boot();
await app.start();
const url = app.restServer.url;
console.log(`Server running at ${url}`);
}
main();
The REST Explorer app will now be hosted on your local machine - http://[::1]:3000/explorer.
RestExplorerComponent
is an example of a LoopBack Component. I will be covering them in one of my upcoming tutorials.
Revisiting RestApplication#
Since RestApplication
is the basis of our framework, let's get a good picture about what we are dealing with - without getting overwhelmed with too much details, of course.
RestApplication Options#
The RestApplication
constructor accepts an options object for configuring the behavior of the application.
It may sound a little confusing, but most of the configuration you will do, will be on a property named rest
instead of the root of this options object. I am in favor of moving the configurations to the root object, but for now it is going to remain as it is till we can come up with a good proposal.
Here are some of the rest
properties that you may find useful.
host
- server host.port
- server port.basePath
- path where to mount the app. For example you may want/api
, instead of the default/
. Beware,app.basePath()
can overwrite it.expressSettings
- settings for the underlying Express server. Watch out for304
responses before concluding, "it does not work!".listenOnStart
- whether to listen after starting. Useful for mounting a LoopBack REST server as a route on an Express application.cors
- CORS configuration.gracePeriodForClose
- number of milliseconds to wait for in-flight requests to be served before stopping the server.openApiSpec
- for customizing how OpenAPI specs are served.disabled
- to disable/openapi.json
. Setting this totrue
will also disable the default API Explorer. As of today, it doesn't have any effect on the API Explorer component, I am suggesting to change it.
apiExplorer
- Surpise! Surprise! This has nothing to do with the API Explorer component you installed; this is for the default hosted API Explorer you saw earlier.url
- path of API Explorer, it can be relative or absolute.disabled
- to disable the default API Explorer. If you aren't using the API Explorer component and don't want the hosted API Explorer either, you should set this totrue
.
You may also be interested in the shutdown
property, particularly the gracePeriod
value, which is the number of milliseconds to wait for the graceful shutdown to finish before exiting the process.
Here is an example of a RestApplication
options object.
{
rest: {
host: 'localhost',
port: 3001,
expressSettings: {
'x-powered-by': false
}
},
shutdown: {
gracePeriod: 1000
}
}
RestApplication Properties#
Once we have an instance of RestApplication
(composed with mixins), we can use the following properties for more information about it.
bootOptions
- added byBootMixin
, configures booters.projectRoot
- added byBootMixin
, specifies the root directory of the project.options
- the options object that was applied to the application.state
- the state of the app - "starting", "starting", "booting", "started".restServer
- theRestServer
instance of the application.config
- augmented re-interpretation ofoptions
.listening
- whether the server is listening or not.url
- the base url for the server, including thebasePath
.rootUrl
- the root url for the server without thebasePath
.httpServer
- the underlyingHttpServer
instance. Note, this is available only after the app starts.
RestApplication Methods#
Here are some of the RestApplication
methods (including those added by mixins and inherited) that you may find useful.
Please note, in some cases there will be more method signatures than what's shown below, for more details refer to the implementing classes - RestApplication, Application, and Context.
General
boot()
- bootstraps the app.start()
- starts the app.stop()
- stops the app.basePath(<path>)
- path where to mount the app. For example you may want/api
, instead of the default/
.static(<requestPath>, <localPath>)
- serves files fromlocalPath
atrequestPath
as static files.redirect(<requestPath>, <redirectedPath>, [statusCode])
- redirects requests torequestPath
toredirectedPath
, using the status code ofstatusCode
(default 303).expressMiddleware(<key>, <middlware>, [options])
- binds an Expressmiddleware
to the applications's context atkey
withoptions
binding options. Fore more information refer to "Using Express middleware ".mountExpressRouter(<basePath>, <router>, [spec])
- mounts an Expressrouter
withspec
atbasePath
to expose additional REST endpoints handled via legacy Express-based stack.middleware(<middleware>, [options])
- registers a LoopBack middleware function or provider class.route(<verb>, <path>, <spec>, <handler>)
- registers a new route for the methodverb
atpath
with the OASspec
with the handlerhandler
.component(<component>, [options])
- adds thecomponent
to the app with theoptions
.controller(<controlle>, [options])
- registers a controller class withoptions
.lifeCycleObserver(<ctor>, [nameOrOptions])
- adds a life cycle observerctor
with thenameOrOptions
options.service(<cls>, [nameOrOptions])
- adds a servicecls
with thenameOrOptions
options.interceptor(<interceptor>)
- registers an interceptor.exportOpenApiSpec([outFile], [logger])
- exports the OpenAPI spec to theoutFile
file, and logs the process usinglogger
function.
Binding related (inherited from Context)
configure(<target>).to(<options>)
- configures thetarget
binding to the specifiedoptions
. This may sound a little confusing, will cover the details in one of the upcoming posts.bind(<key>)
- creates a binding with the givenkey
and adds it to the application context.unbind(<binding>)
- unbinds a binding from the application context.add(<binding>)
- adds a binding to the application context.find(<pattern|filter>)
- finds bindings using a keypattern
orfilter
function from the application context.findByTag(<filter>)
- finds bindings using the tagfilter
from the application context.get(<key>)
- gets the value bound to the givenkey
from the application context.getSync(<key>)
- synchronous get the value bound to the givenkey
from the application context.getBinding(<key>)
- looks up a binding bykey
in the context and its ancestors from the application context.html).toJSON()
- creates a plain JSON object for the application context. Great for manually looking at the objects that are bound to the context.inspect()
- inspects the context and dump out a JSON object representing the context hierarchy.
RestApplication
is also an EventEmitter (inherited), so all the EventEmitter
methods are available too.
Conclusion#
The intent of this tutorial was to introduce you to LoopBack 4 concepts and its REST framework libraries, not to manually create LoopBack REST apps. So tuck away the project directory we created somewhere safe for now.
"Where do I go from here?"
Scaffold a full-fledged LoopBack REST app project using lb4 [name]
. Let's call this project "cool".
$ lb4 cool
? Project description: cool
? Project root directory: cool
? Application class name: CoolApplication
? Select features to enable in the project Enable eslint, Enable prettier, Enable mocha, Enable loopbackBuild, Enable vscode, Enable docker, Enable repositories, Enable services
? Yarn is available. Do you prefer to use it by default? No
...
Application cool was created in cool.
Next steps:
$ cd cool
$ npm start
Inspect the files and directories in the cool/src
directory, you should now be able to make sense of everything there. Use the lb4
CLI to create the datasource, models, etc., and further experiment.
Summary#
In this tutorial you learned that LoopBack 4 is a super-framework that can be used to create different kinds of apps and frameworks. We assembled a REST framework using the RestApplication
class. You also learnt about the lb4
CLI, which can be used to scaffold LoopBack REST applications and generate its artifacts.
Did you like this tutorial? Follow me on Twitter - @hacksparrow - for more LoopBack, Node.js, and JavaScript tutorials and tips.