Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/site/Creating-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class MyComponent implements Component {
}
```

## Injecting the target application instance

You can inject anything from the context and access them from a component. In
the following example, the REST application instance is made available in the
component via dependency injection.
Expand All @@ -53,6 +55,8 @@ export class MyComponent implements Component {
}
```

## Registration of component artifacts

When a component is mounted to an application, a new instance of the component
class is created and then:

Expand Down
187 changes: 187 additions & 0 deletions docs/site/extending/rest-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
lang: en
title: 'Contributing REST API endpoints'
keywords: LoopBack 4, Extensions, Components
sidebar: lb4_sidebar
permalink: /doc/en/lb4/creating-components-rest-api.html
---

## Overview

As mentioned in [Creating components](../Creating-components.md), components can
contribute new REST API endpoints by adding Controller classes to the list of
controllers that should be mounted on the target application.

```ts
class MyController {
@get('/ping')
ping() {
return {running: true};
}
}

export class MyComponent implements Component {
constructor() {
this.controllers = [MyController];
}
}
```

This is approach works great when the metadata is hard-coded, e.g. the "ping"
endpoint path is always `/ping`.

In practice, components often need to allow applications to configure REST API
endpoints contributed by a component:

- Produce OpenAPI dynamically based on the component configuration, e.g.
customize the path of controller endpoints.

- Exclude certain controller endpoints based on configuration. For example, our
REST API explorer has a flag `useSelfHostedSpec` that controls whether a new
endpoint serving OpenAPI document is added.

## Dynamic OpenAPI metadata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use https://loopback.io/doc/en/lb4/Booting-an-Application.html#boot-an-application-using-component for some of the use cases. I was able to modify the binding key and other metadata for services/controllers/... loaded by booters by override the app.service(), app.controller() for the subapp.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the pointer, I agree "booting an app using component" is a concept that can be useful when migrating LB3 extensions to LB4. However, I don't see how to apply it to "Dynamic OpenAPI metadata". I opened a new issue to document "booting an app using component" for extension authors in a holistic way - see #5422


In order to access component configuration when defining Controller OpenAPI
metadata, the component has to change the way how Controller classes are
defined. Instead of defining controllers as static classes, a factory function
should be introduced. This approach is already used by
[`@loopback/rest-crud`](https://github.com/strongloop/loopback-next/tree/master/packages/rest-crud)
to create dynamic CRUD REST endpoints for a given model.

Example showing a component exporting a `/ping` endpoint at a configurable base
path:

```ts
import {config} from '@loopback/context';
import {Component} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
import {MyComponentBindings} from './my-component.keys.ts';
import {definePingController} from './controllers/ping.controller-factory.ts';

@bind({tags: {[ContextTags.KEY]: MyComponentBindings.COMPONENT.key}})
export class MyComponent implements Component {
constructor(
@config(),
config: MyComponentConfig = {},
) {
const basePath = this.config.basePath ?? '';
this.controller = [definePingController(basePath)];
}
}
```

Example implementation of a controller factory function:

```ts
import {get} from '@loopback/rest';
import {Constructor} from '@loopback/core';

export function definePingController(basePath: string): Constructor<unknown> {
class PingController {
@get(`${basePath}/ping`)
ping() {
return {running: true};
}
}

return PingController;
}
```

## Optional endpoints

We recommend components to group optional endpoints to standalone Controller
classes, so that an entire controller class can be added or not added to the
target application, depending on the configuration.

The example below shows a component that always contributed a `ping` endpoint
and sometimes contributes a `stats` endpoint, depending on the configuration.

```ts
import {bind, config, ContextTags} from '@loopback/context';
import {MyComponentBindings} from './my-component.keys.ts';
import {PingController, StatsController} from './controllers';

export interface MyComponentConfig {
stats: boolean;
}

@bind({tags: {[ContextTags.KEY]: MyComponentBindings.COMPONENT.key}})
export class MyComponent implements Component {
constructor(
@config()
config: MyComponentConfig = {},
) {
this.controllers = [PingController];
if (config.stats) this.controllers.push(StatsController);
}
}
```

## Undocumented endpoints

Sometimes it's desirable to treat the new endpoints as internal (undocumented)
and leave them out from the OpenAPI document describing application's REST API.
This can be achieved using custom LoopBack extension `x-visibility`.

```ts
class MyController {
// constructor

@get('/health', {
responses: {},
'x-visibility': 'undocumented',
})
health() {
// ...
}
}
```

## Express routes

Sometimes it's not feasible to implement REST endpoints as LoopBack Controllers
and components need to contribute Express routes instead.

1. Modify your component class to receive the target application via constructor
dependency injection, as described in
[Injecting the target application instance](../Creating-components.md#injecting-the-target-application-instance).

2. In your extension, create a new `express.Router` instance and define your
REST API endpoints on that router instance using Express API like
[`router.use()`](https://expressjs.com/en/4x/api.html#router.use),
[`router.get()`](https://expressjs.com/en/4x/api.html#router.METHOD),
[`router.post()`](https://expressjs.com/en/4x/api.html#router.METHOD), etc.

3. Call `app.mountExpressRouter()` to add the Express router to the target
application. Refer to
[Mounting an Express router](https://loopback.io/doc/en/lb4/Routes.html#mounting-an-express-router)
for more details.

Example component:

```ts
import {Component, CoreBindings, inject} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
import express from 'express';

export class MyComponent implements Component {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
private application: RestApplication,
) {
const router = express.Router();
this.setupExpressRoutes(router);
application.mountExpressRouter('/basepath', router, {
// optional openapi spec
});
}

setupExpressRoutes(router: express.Router) {
router.get('/hello', (req, res, next) => {
res.json({msg: 'hello'});
});
}
}
```
43 changes: 43 additions & 0 deletions docs/site/extending/services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
lang: en
title: 'Creating services in components'
keywords: LoopBack 4, Extensions, Components, Services
sidebar: lb4_sidebar
permalink: /doc/en/lb4/creating-components-services.html
---

In LoopBack 4, a Service class provides access to additional functionality.

- Local services are used to implement "utility" functionality, for example
obtain a JWT authentication token for a given user.
- Service proxies are used to access 3rd-party web services (e.g. REST or SOAP),
as further explained in
[Calling other APIs and web services](../Calling-other-APIs-and-Web-Services.md)

In an application, a new service is typically created by running
[`lb4 service`](../Service-generator.md).

Components can contribute local services as follows.

1. Run [`lb4 service`](../Service-generator.md) and choose either
`Local service class` or `Local service provider` as the service type to
create.
2. In your component constructor, create a service binding and add it to the
list of bindings contributed by the component to the target application
class.

An example showing how to build a component contributing a local service class
(`MyService`) and a local service provider (`GeocodeServiceProvider`):

```ts
import {createServiceBinding} from '@loopback/core';
import {MyService} from './services/my.service.ts';
import {GeocodeServiceProvider} from './services/geocoder.service.ts';

export class SampleComponent implements Component {
bindings = [
createServiceBinding(MyService),
createServiceBinding(GeocoderServiceProvider),
];
}
```
2 changes: 2 additions & 0 deletions docs/site/migration/components/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,7 @@ instructions into several sub-sections.
1. [Migrating components contributing Model mixins](./mixins) explains how to
migrate a component that's contributing Model mixins.

1. [Migrating components contributing REST API endpoints](./rest-api)

1. _More sections will be created as part of
[loopback-next#3955](https://github.com/strongloop/loopback-next/issues/3955)._
Loading