A basic level knowledge of TypeScript is a minimal requirement for using LoopBack 4. If you are totally new to TypeScript, I suggest you learn it and come back. If you know JavaScript already, it can be leaned in a day.

As one of the core developers of LoopBack, a very frequent complaint we hear is about our "not-so-great" documentation. We are working on it.

While the official effort is on, I have decided to put some extra personal effort and start creating LoopBack 4 learning material on my website - written in my own style.

Welcome to my LoopBack 4 tutorial. Let's begin!

What is LoopBack 4?#

There are two things you must know right at the start.

  1. LoopBack 4 as an entity does not exist.
  2. 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.

While the 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 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.

Assembling a REST framework#

Begin by creating 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 invoke ts-node.

Add a tsconfig.json file in the directory with the following content.

tsconfig.json
{
  "$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 this 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.

index.ts
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

If you want to customize the port number and host, specify an application config object as shown below.

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.

  1. Model - stores the data
  2. View - composes query results
  3. 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.

index.ts
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

If you are new to dependency injection, think of "injecting" as making a value from a well-managed global registry (Context) available in a class - via its constructor, properties or method arguments - by the class itself.

The objects in the Context are referred to, 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 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.

index.ts
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.

index.ts
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}!`);
  }
}
...

LoopBack uses Express for HTTP processing at the lower levels. The LoopBack request and response objects are in fact the familiar Express Request and Response objects, respectively.

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 were 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 specifying 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.

  • There are two API endpoints - /greet and /
  • /greet accepts GET requests with a parameter named name in the query string, and returns an object with name (string) and number (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.

You can 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 on 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?"

Instead, how about we create a directory named controllers, keep the controller files there and let LoopBack load all of them for us?

"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.

Mixins are software composability design pattern. In LoopBack, they are implemented as functions that add new properties and methods to a class.

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 the 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 parent package @loopback/build as a development dependency.

$ npm i -D @loopback/build

Update the tsconfig.json for the new changes.

tsconfig.json
{
  "$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 be a class which extends RestApplication.

Extending RestApplication#

Here is the new index.ts file in the src directory.

src/index.ts
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();

this.projectRoot is added by BootMixin, and is a requirement.

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.

src/controllers/greet.controller.ts
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.

src/controllers/index.ts
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.

package.json
{
  "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 not 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.

models/author.model.ts
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.

models/index.ts
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.

src/index.ts
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.

Most connectors provide a method for executing raw queries, using which, users can craft their own queries if required.

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.

The lb4 CLI can scaffold a whole project too, not just generate models, datasources, repositories, and controllers. For real projects, 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 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.

Rest Explorer

Play around with your REST API!

API endpoints are generated based on available controllers, merely creating model files will not generate them. So if you don't see your models in the API Explorer; maybe you did not create controllers for them?

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. Creating model relations in LoopBack is very easy.

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 playing with 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:

  1. Adds a foreign key named authorId to the Book model, decorated with the @belongsTo() decorator; take a look at the book.model.ts file.
  2. Adds the implementation details of the BelongsTo relation in the book.repository.ts file.
  3. 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.

In non-SQL databases like MongoDB, In-memory db (used in this tutorial) etc., you will be able to create instances of Book with non-existing authorIds. 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:

  1. Adds a new property named books to the Author model, decorated with the @hasMany() decorator; take a look at the author.model.ts file.
  2. Adds the implementation details of the HasMany relation in the author.repository.ts file.
  3. 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 for a local instance of API Explorer.

Install it.

$ npm i @loopback/rest-explorer

Then load it in your app; update the index.ts file as shown below. Restart the app.

src/index.ts
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 for 304 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. Beware, CORS is enable by default. I am proposing to change it. For config options refer to expressjs/cors.
  • 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 to true 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 to true.

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 by BootMixin, configures booters.
  • projectRoot - added by BootMixin, 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 - the RestServer instance of the application.
    • config - augmented re-interpretation of options.
    • listening - whether the server is listening or not.
    • url - the base url for the server, including the basePath.
    • rootUrl - the root url for the server without the basePath.
    • httpServer - the underlying HttpServer 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 from localPath at requestPath as static files.
  • redirect(<requestPath>, <redirectedPath>, [statusCode]) - redirects requests to requestPath to redirectedPath, using the status code of statusCode (default 303).
  • expressMiddleware(<key>, <middlware>, [options]) - binds an Express middleware to the applications's context at key with options binding options. Fore more information refer to "Using Express middleware ".
  • mountExpressRouter(<basePath>, <router>, [spec]) - mounts an Express router with spec at basePath 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 method verb at path with the OAS spec with the handler handler.
  • component(<component>, [options]) - adds the component to the app with the options.
  • controller(<controlle>, [options]) - registers a controller class with options.
  • lifeCycleObserver(<ctor>, [nameOrOptions]) - adds a life cycle observer ctor with the nameOrOptions options.
  • service(<cls>, [nameOrOptions]) - adds a service cls with the nameOrOptions options.
  • interceptor(<interceptor>) - registers an interceptor.
  • exportOpenApiSpec([outFile], [logger]) - exports the OpenAPI spec to the outFile file, and logs the process using logger function.

Binding related (inherited from Context)

  • configure(<target>).to(<options>) - configures the target binding to the specified options. This may sound a little confusing, will cover the details in one of the upcoming posts.
  • bind(<key>) - creates a binding with the given key 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 key pattern or filter function from the application context.
  • findByTag(<filter>) - finds bindings using the tag filter from the application context.
  • get(<key>) - gets the value bound to the given key from the application context.
  • getSync(<key>) - synchronous get the value bound to the given key from the application context.
  • getBinding(<key>) - looks up a binding by key 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, and 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 meta-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.

Tweet this | Share on LinkedIn |