-
Notifications
You must be signed in to change notification settings - Fork 1.1k
docs: create REST API endpoints in components + LB3 migration #5385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| 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'}); | ||
| }); | ||
| } | ||
| } | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| ]; | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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