From 1dec4db5e9192a021527be16ed0b2309b4555f3c Mon Sep 17 00:00:00 2001 From: Daniel Bravo Date: Wed, 22 Sep 2021 17:18:03 -0400 Subject: [PATCH 1/2] Create a proxying tutorial Signed-off-by: Michael Stergianis Signed-off-by: Daniel Bravo --- .../using-backstage-proxy-within-plugin.md | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/tutorials/using-backstage-proxy-within-plugin.md diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md new file mode 100644 index 0000000000..64a0451a90 --- /dev/null +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -0,0 +1,210 @@ +--- +id: using-backstage-proxy-within-plugin +title: Using the Backstage Proxy from Within a Plugin +# prettier-ignore +description: Guide on how to create a set of API bindings that interface with a backend via the backstage proxy +--- + +This guide walks you through setting up a simple proxy to an existing API that +is deployed externally to backstage and sending requests to that API from within +a backstage frontend plugin. For a more detailed description of the APIs used +here, check out the [Utility APIs section](../api/utility-apis.md). + +If your plugin requires access to an API, backstage offers +[3 options](../plugins/call-existing-api.md): + +1. you can + [access the API directly](../plugins/call-existing-api.md#issuing-requests-directly), +1. you can create a [backend plugin](../plugins/backend-plugin.md) if you are + implementing the API alongside your frontend plugin +1. you can configure backstage to proxy to an already existing API. The third + approach tends to be the most popular, since you likely already have an + existing API, and it allows you to develop/deploy your API in any way you + want. + +**Table of Contents** + +- [Setting up the backstage proxy](#setting-up-the-backstage-proxy) +- [Calling an API using the backstage proxy](#calling-an-api-using-the-backstage-proxy) + - [Defining the API client interface](#defining-the-api-client-interface) + - [Creating the API client](#creating-the-api-client) + - [Bundling your ApiRef with your plugin](#bundling-your-apiref-with-your-plugin) + - [Using your plugin in your components](#using-your-plugin-in-your-components) + +# Setting up the backstage proxy + +Let's say your plugin's API is hosted at _https://api.myawesomeservice.com/v1_, +and you want to be able to access it within backstage at +`/api/proxy/`, and add a default header called +`X-Custom-Source`. You will need to add the following to `app-config.yaml`: + +```yaml +proxy: + '/': + target: https://api.myawesomeservice.com/v1 + changeOrigin: true # Use this to avoid cors related issues + headers: + X-Custom-Source: backstage +``` + +You can find more details about the proxy config options in the +[proxying section](../plugins/proxying.md). + +# Calling an API using the backstage proxy + +If you followed the previous steps, you should now be able to access your API by +calling `${backend-url}/api/proxy/`. The reason why +`backend-url` is referenced is because the backstage backend creates and runs +the proxy. Backstage is structured in such a way that you could run the +backstage frontend independently of the backend. So when calling your API you +need to prepend the backend url to your http call. The supported way of doing +this, is to use [`createApiRef`](../reference/core-plugin-api.createapiref.md) +to create a new [`ApiRef`](../reference/core-plugin-api.apiref.md) and register +an [`ApiFactory`](../reference/core-plugin-api.apifactory.md), which will wrap +your API implementation, with your plugin using +[`createApiFactory`](../reference/core-plugin-api.createapifactory.md). Then, +you can use your API in your components by calling +[`useApi`](../reference/core-plugin-api.useapi.md) + +## Defining the API client interface + +Continuing from the previous example, let's assume that +_https://api.myawesomeservice.com/v1_ has the following endpoints: + +| Method | Description | +| :----------------------- | :---------------------- | +| `GET /users` | Returns a list of users | +| `GET /users/{userId}` | Returns a single user | +| `DELETE /users/{userId}` | Deletes a user | + +Here is an example definition for this API following backstage's `apiRef` style: + +```ts +/* src/api.ts */ +import { createApiRef } from '@backstage/core-plugin-api'; + +export interface User { + name: string; + email: string; +} + +export interface MyAwesomeApi { + url: string; + listUsers: () => Promise>; + getUser: (userId: string) => Promise; + deleteUser: (userId: string) => Promise; +} + +export const myAwesomeApiRef = createApiRef({ + id: 'plugin.my-awesome-api.service', + description: 'Example API definition', +}); +``` + +## Creating the API client + +The `myAwesomeApiRef` is what you will use within backstage to reference the API +client in your plugin. The API ref itself is a global singleton object that +allows you to reference your instantiated API. The actual implementation would +look something like this: + +```ts +/* src/api.ts */ + +/* ... */ + +import { Config } from '@backstage/config'; + +export class MyAwesomeApiClient implements MyAwesomeApi { + static fromConfig(config: Config) { + return new MyAwesomeApiClient(config.getString('backend.baseUrl')); + } + + constructor(public url: string) {} + + private async fetch(input: string, init?: RequestInit): Promise { + // As configured previously for the backend proxy + const proxyUri = '/api/proxy/' + + const resp = await fetch(`${this.url}${proxyUri}${input}`, init); + if (!resp.ok) throw new Error(resp); + return await resp.json(); + } + + async listUsers(): Promise> { + return await this.fetch>('/users'); + } + + async getUser(userId: string): Promise { + return await this.fetch(`/users/${userId}`); + } + + async deleteUser(userId: string): Promise { + return await this.fetch( + `/users/${userId}`, + { method: 'DELETE' } + ); + } +``` + +> If you want to understand how the Config object works at a deeper level, check +> this [doc](../conf/reading.md) + +## Bundling your ApiRef with your plugin + +The final piece in the puzzle is bundling the `myAwesomeApiRef` with a factory +for `MyAwesomeApiClient` objects. This is usually done in the `plugin.ts` file +inside the plugin's `src` directory. This is an example of what it'd look like, +assuming you added the previous code in a file called `api.ts`: + +```ts +/* src/plugin.ts */ +import { myAwesomeApiRef, MyAwesomeApiClient } from './api'; +import { rootRouteRef } from './routes'; +import { + createPlugin, + createRouteRef, + createApiFactory, + configApiRef, + createRoutableExtension, + createComponentExtension, +} from '@backstage/core-plugin-api'; + +//... + +export const myCustomPlugin = createPlugin({ + id: '', + routes: { + root: rootRouteRef, + }, + + // Configure a factory for myAwesomeApiRef + apis: [ + createApiFactory({ + api: myAwesomeApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => MyAwesomeApiClient.fromConfig(configApi), + }), + ], +}); +``` + +## Using your plugin in your components + +Now you should be able to access your API using the backstage hook +[`useApi`](../reference/core-plugin-api.useapi.md) from within your plugin code. + +```ts +/* plugins/my-awesome-plugin/src/components/AwesomeUsersTable.tsx */ +import { useApi } from '@backstage/core-plugin-api'; +import { myAwesomeApiRef } from '../../api'; + +export const AwesomeUsersTable = () => { + const apiClient = useApi(myAwesomeApiRef); + + apiClient.listUsers() + .then( + ... + ) +} +``` From a422339a548ed5049ff9b9105d650ad2acad688e Mon Sep 17 00:00:00 2001 From: Michael Stergianis Date: Mon, 4 Oct 2021 15:02:23 -0400 Subject: [PATCH 2/2] Update proxying tutorial to address feedback Signed-off-by: Michael Stergianis --- .../using-backstage-proxy-within-plugin.md | 64 +++++++++---------- microsite/sidebars.json | 3 +- mkdocs.yml | 1 + 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md index 64a0451a90..b0c6bef8ef 100644 --- a/docs/tutorials/using-backstage-proxy-within-plugin.md +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -7,8 +7,7 @@ description: Guide on how to create a set of API bindings that interface with a This guide walks you through setting up a simple proxy to an existing API that is deployed externally to backstage and sending requests to that API from within -a backstage frontend plugin. For a more detailed description of the APIs used -here, check out the [Utility APIs section](../api/utility-apis.md). +a backstage frontend plugin. If your plugin requires access to an API, backstage offers [3 options](../plugins/call-existing-api.md): @@ -17,10 +16,7 @@ If your plugin requires access to an API, backstage offers [access the API directly](../plugins/call-existing-api.md#issuing-requests-directly), 1. you can create a [backend plugin](../plugins/backend-plugin.md) if you are implementing the API alongside your frontend plugin -1. you can configure backstage to proxy to an already existing API. The third - approach tends to be the most popular, since you likely already have an - existing API, and it allows you to develop/deploy your API in any way you - want. +1. you can configure backstage to proxy to an already existing API. **Table of Contents** @@ -29,7 +25,7 @@ If your plugin requires access to an API, backstage offers - [Defining the API client interface](#defining-the-api-client-interface) - [Creating the API client](#creating-the-api-client) - [Bundling your ApiRef with your plugin](#bundling-your-apiref-with-your-plugin) - - [Using your plugin in your components](#using-your-plugin-in-your-components) + - [Using the API in your components](#using-your-plugin-in-your-components) # Setting up the backstage proxy @@ -42,7 +38,6 @@ and you want to be able to access it within backstage at proxy: '/': target: https://api.myawesomeservice.com/v1 - changeOrigin: true # Use this to avoid cors related issues headers: X-Custom-Source: backstage ``` @@ -57,14 +52,21 @@ calling `${backend-url}/api/proxy/`. The reason why `backend-url` is referenced is because the backstage backend creates and runs the proxy. Backstage is structured in such a way that you could run the backstage frontend independently of the backend. So when calling your API you -need to prepend the backend url to your http call. The supported way of doing -this, is to use [`createApiRef`](../reference/core-plugin-api.createapiref.md) -to create a new [`ApiRef`](../reference/core-plugin-api.apiref.md) and register -an [`ApiFactory`](../reference/core-plugin-api.apifactory.md), which will wrap -your API implementation, with your plugin using -[`createApiFactory`](../reference/core-plugin-api.createapifactory.md). Then, -you can use your API in your components by calling -[`useApi`](../reference/core-plugin-api.useapi.md) +need to prepend the backend url to your http call. + +The recommended pattern for calling out to services is to wrap your calls in a +[Utility API](../api/utility-apis.md). This section describes the steps to wrap +your API client in a Utility API, which are: + +- use [`createApiRef`](../reference/core-plugin-api.createapiref.md) to create a + new [`ApiRef`](../reference/core-plugin-api.apiref.md) +- register an [`ApiFactory`](../reference/core-plugin-api.apifactory.md) with + your plugin using + [`createApiFactory`](../reference/core-plugin-api.createapifactory.md). This + will wrap your API implementation, associate your `ApiRef` with your + implementation and tell backstage how to instantiate it +- finally, you can use your API in your components by calling + [`useApi`](../reference/core-plugin-api.useapi.md) ## Defining the API client interface @@ -113,20 +115,20 @@ look something like this: /* ... */ -import { Config } from '@backstage/config'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; export class MyAwesomeApiClient implements MyAwesomeApi { - static fromConfig(config: Config) { - return new MyAwesomeApiClient(config.getString('backend.baseUrl')); - } + discoveryApi: DiscoveryApi; - constructor(public url: string) {} + constructor({discoveryApi}: {discoveryApi: DiscoveryApi}) { + this.discoveryApi = discoveryApi; + } private async fetch(input: string, init?: RequestInit): Promise { // As configured previously for the backend proxy - const proxyUri = '/api/proxy/' + const proxyUri = '${await this.discoveryApi.getBaseUrl('proxy')}/'; - const resp = await fetch(`${this.url}${proxyUri}${input}`, init); + const resp = await fetch(`${proxyUri}${input}`, init); if (!resp.ok) throw new Error(resp); return await resp.json(); } @@ -147,8 +149,8 @@ export class MyAwesomeApiClient implements MyAwesomeApi { } ``` -> If you want to understand how the Config object works at a deeper level, check -> this [doc](../conf/reading.md) +> For more information on the DiscoveryApi check out the +> [docs](../reference/core-plugin-api.discoveryapi.md) ## Bundling your ApiRef with your plugin @@ -160,36 +162,32 @@ assuming you added the previous code in a file called `api.ts`: ```ts /* src/plugin.ts */ import { myAwesomeApiRef, MyAwesomeApiClient } from './api'; -import { rootRouteRef } from './routes'; import { createPlugin, createRouteRef, createApiFactory, - configApiRef, createRoutableExtension, createComponentExtension, + discoveryApiRef, } from '@backstage/core-plugin-api'; //... export const myCustomPlugin = createPlugin({ id: '', - routes: { - root: rootRouteRef, - }, // Configure a factory for myAwesomeApiRef apis: [ createApiFactory({ api: myAwesomeApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => MyAwesomeApiClient.fromConfig(configApi), + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new MyAwesomeApiClient({ discoveryApi }), }), ], }); ``` -## Using your plugin in your components +## Using the API in your components Now you should be able to access your API using the backstage hook [`useApi`](../reference/core-plugin-api.useapi.md) from within your plugin code. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 2d14a288d8..9cf3b204c2 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -255,7 +255,8 @@ "tutorials/quickstart-app-plugin", "tutorials/migrating-away-from-core", "tutorials/configuring-plugin-databases", - "tutorials/switching-sqlite-postgres" + "tutorials/switching-sqlite-postgres", + "tutorials/using-backstage-proxy-within-plugin" ], "Architecture Decision Records (ADRs)": [ "architecture-decisions/adrs-overview", diff --git a/mkdocs.yml b/mkdocs.yml index 48d1d49dcf..bb2d7b7fba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -165,6 +165,7 @@ nav: - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' - Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md' - Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md' + - Using the Backstage Proxy from Within a Plugin: 'tutorials/using-backstage-proxy-within-plugin.md' - Architecture Decision Records (ADRs): - Overview: 'architecture-decisions/index.md' - ADR001 - Architecture Decision Record (ADR) log: 'architecture-decisions/adr001-add-adr-log.md'