diff --git a/docs/site/Creating-components.md b/docs/site/Creating-components.md index 289c44f6fd56..be5563996df5 100644 --- a/docs/site/Creating-components.md +++ b/docs/site/Creating-components.md @@ -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. @@ -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: diff --git a/docs/site/extending/rest-api.md b/docs/site/extending/rest-api.md new file mode 100644 index 000000000000..3d92b2b46f79 --- /dev/null +++ b/docs/site/extending/rest-api.md @@ -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 + +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 { + 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'}); + }); + } +} +``` diff --git a/docs/site/extending/services.md b/docs/site/extending/services.md new file mode 100644 index 000000000000..ebf0e0f28cf8 --- /dev/null +++ b/docs/site/extending/services.md @@ -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), + ]; +} +``` diff --git a/docs/site/migration/components/overview.md b/docs/site/migration/components/overview.md index 2e934cd8dfa1..2f58b9f4ffbd 100644 --- a/docs/site/migration/components/overview.md +++ b/docs/site/migration/components/overview.md @@ -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)._ diff --git a/docs/site/migration/components/rest-api.md b/docs/site/migration/components/rest-api.md new file mode 100644 index 000000000000..6454c5b48562 --- /dev/null +++ b/docs/site/migration/components/rest-api.md @@ -0,0 +1,202 @@ +--- +lang: en +title: 'Migrating components: REST API endpoints' +keywords: LoopBack 4, LoopBack 3, Migration, Extensions, Components +sidebar: lb4_sidebar +permalink: /doc/en/lb4/migration-extensions-rest-api.html +--- + +{% include tip.html content=" +Missing instructions for your LoopBack 3 use case? Please report a [Migration docs issue](https://github.com/strongloop/loopback-next/issues/new?labels=question,Migration,Docs&template=Migration_docs.md) on GitHub to let us know. +" %} + +Please get yourself familiar with +[Contributing REST API endpoints](../../extending/rest-api.md) first. It's +important to understand different ways how LoopBack 4 components can contribute +REST API endpoints to target applications, before learning about the migration +from LoopBack 3. + +In our research of existing LoopBack 3 components, we have identified several +different use cases: + +1. Add a new REST API endpoint at a configured path, e.g. `/visualize` returning + an HTML page. + +2. REST API endpoints providing file upload & download. + +3. Add a new local service (e.g. `Ping.ping()`) and expose it via REST API (e.g. + `/ping`), allow the user to customize the base path where the API is exposed + at (e.g. `/pong`). + +4. Add new REST API endpoints using Express `(req, res, next)` style. + +In the following text, we will describe how to migrate these use cases from +LoopBack 3 style to LoopBack 4. + +## General instructions + +There are different ways how a LoopBack 3 component can contribute new REST API +endpoints: + +- Some extensions are adding remote methods to existing or newly-created models. +- Other extensions are contributing Express-style routes. + +We recommend extension authors to convert their LoopBack 3 routes to LoopBack 4 +Controller methods wherever possible. + +1. Create a new Controller class and add it to your component, see + [Contributing REST API endpoints](../../extending/rest-api.md#overview). + +2. Move your LoopBack 3 REST API endpoints to the new controller class. + +3. Decide if you want these endpoints included in OpenAPI spec. Follow the steps + in + [Undocumented endpoints](../../extending/rest-api.md#undocumented-endpoints) + to hide the new endpoints from the OpenAPI spec describing the target + application's API. + +4. Optionally, if you want to make your REST endpoints configurable by target + applications, then follow the steps in + [Dynamic OpenAPI metadata](../../extending/rest-api.md#dynamic-openapi-metadata) + to wrap you controller method in a class factory function. + +## Migrating REST API endpoints returning HTML pages + +To migrate an endpoint that's returning files (or any other non-JSON payload), +please follow [General instructions](#general-instructions) and then perform two +additional steps: + +1. Modify the controller constructor to inject the HTTP response. + + ```ts + import {RestBindings, Response} from '@loopback/rest'; + + class MyController { + constructor(@inject(RestBindings.Http.RESPONSE) response: Response) {} + } + ``` + +2. In the LoopBack 4 controller method, use Express API like `res.contentType()` + and `res.send()` to send back the result. (This is a workaround until + [loopback-next#436](https://github.com/strongloop/loopback-next/issues/436) + is implemented). + +## Migrating file upload & download + +1. Follow [General instructions](#general-instructions) to create LoopBack 4 + controllers for your endpoints. + +2. Follow the instructions in + [Upload and download files ](../../File-upload-download.md) to implement file + upload & download functionality in the migrated endpoints. + +## Migrating Express routes + +We recommend extension authors to use +[Controllers](https://loopback.io/doc/en/lb4/Controllers.html) as a better +alternative to low-level Express route API. + +If it's not feasible to implement REST endpoints as LoopBack Controllers: + +1. Follow the instructions in + [Express routes](../../extending/rest-api.md#express-routes) to setup an + Express router in your LoopBack 4 component and contribute it to the target + application. + +2. Take the implementation of Express routes from your LoopBack 3 component and + mount these routes on the Express router created in the previous step, as + explained in [Express routes](../../extending/rest-api.md#express-routes). + +## Migrating local services exposed via REST API + +Consider the following scenario: + +- A LoopBack 3 component is contributing a new Model (e.g. `Ping`) providing + various local services (e.g. `Ping.ping()`) and exposing these services via + REST API (e.g. at `GET /ping`). + +How to migrate such components: + +1. Follow the steps in + [Creating services in components](../../extending/services.md) to add a new + local service to your LoopBack 4 component. + +2. Move the implementation of your LoopBack 3 service model (e.g. `Ping`) to the + newly created LoopBack 4 local service class. + +3. Follow the steps in + [Contributing REST API endpoints](../../extending/rest-api.md) to add a new + controller class to your LoopBack 4 component. + +4. Use [Dependency Injection](../../Dependency-injection.md) to inject an + instance of the local service into the controller class. + +5. For each service method you want to expose via REST API, implement a new + controller method calling the Service class API under the hood. + +An example implementation of the ping service class: + +```ts +import {bind, BindingScope} from '@loopback/core'; + +@bind({scope: BindingScope.TRANSIENT}) +export class PingService { + ping() { + return {status: 'ok'}; + } +} +``` + +An example implementation of the ping controller factory: + +```ts +import {bind, BindingScope, Constructor, inject} from '@loopback/core'; +import {get} from '@loopback/rest'; +import {PingBindings} from '../ping.keys'; +import {PingService} from '../services/ping.service.ts'; + +export function definePingController(basePath: string): Constructor { + @bind({scope: BindingScope.SINGLETON}) + class PingController { + constructor( + @inject(PingBindings.PING_SERVICE) + private pingService: PingService, + ) {} + + @get(`${basePath}/ping`) + ping() { + return this.pingService.ping(); + } + } + + return PingController; +} +``` + +An example component using contributing a ping service exposed via REST, using +the local service and the controller factory implemented in code snippets above: + +```ts +import {createServiceBinding} from '@loopback/core'; +import {definePingController} from './controllers/ping.controller.ts'; +import {PingBindings} from './keys'; +import {PingService} from './services/ping.service.ts'; +import {PingComponentConfig} from './types'; + +@bind({tags: {[ContextTags.KEY]: PingBindings.COMPONENT.key}}) +export class PingComponent implements Component { + constructor( + @config(), + config: PingComponentConfig = {}, + ) { + this.bindings = [ + createServiceBinding(PingService), + ]; + + const basePath = this.config.basePath ?? ''; + this.controllers = [ + definePingController(basePath) + ]; + } +} +``` diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index 1b67e5981969..a8cb0b9f08a2 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -359,6 +359,14 @@ children: url: Creating-decorators.html output: 'web, pdf' + - title: 'Contributing REST API' + url: 'creating-components-rest-api.html' + output: 'web, pdf' + + - title: 'Creating services' + url: 'creating-components-services.html' + output: 'web, pdf' + - title: 'Creating servers' url: Creating-servers.html output: 'web, pdf' @@ -893,6 +901,10 @@ children: url: migration-extensions-mixins.html output: 'web, pdf' + - title: 'REST API' + url: migration-extensions-rest-api.html + output: 'web, pdf' + - title: 'Clients (API consumers)' url: migration-clients.html output: 'web, pdf' diff --git a/docs/spikes/2020-04-how-to-migrate-lb3-components.md b/docs/spikes/2020-04-how-to-migrate-lb3-components.md index 4d09e0295f07..a4e75d9041c7 100644 --- a/docs/spikes/2020-04-how-to-migrate-lb3-components.md +++ b/docs/spikes/2020-04-how-to-migrate-lb3-components.md @@ -225,7 +225,37 @@ TBD ### Migrate REST API -TBD +See [Extending REST API endpoints](../site/extending/rest-api.md) for +instructions how to write components contributing REST API endpoints. + +See +[Migrating components: REST API endpoints](../site/migration/components/rest-api.md) +for migration steps. + +> Add a new REST API endpoint to every (public) model/entity provided by the +> app, e.g. `GET /api/{model}/schema` returning JSON schema of the model. + +First, we need to find all publicly exposed models. This is tricky in LB4, +because there is no direct mapping between models and REST API endpoints. REST +APIs are implemented by Controllers, there may be more than one Controller for +each Model (e.g. `ProductController` and `ProductCategoryController`). + +I am arguing that the particular use case of accessing JSON schema of a model is +not relevant in LB4, because model schema can be obtained from the OpenAPI spec +document provided by all LB4 applications out of the box. + +Let's not provide any migration guide for this technique for now. We can wait +until there is user demand and then build a better understanding of user +requirements before looking for LB4 solution. + +> Intercept REST requests handled by an endpoint contributed by a 3rd-party +> controller to invoke additional logic before the controller method is called +> (e.g. automatically create containers when uploading files) and after the +> controller method returned (e.g. post-process the uploaded file). + +Considering that this use case is specific to the way how +loopback-component-storage implements REST API endpoints for file uploads and +downloads, I feel it's not necessary to provide migration guide for this case. ### Migrate Services (local and remote)