Merge pull request #21891 from backstage/freben/more-doc-apis
flesh out the api docs some more
This commit is contained in:
@@ -6,9 +6,7 @@ sidebar_label: Creating APIs
|
||||
description: Creating new utility APIs in your plugins and app
|
||||
---
|
||||
|
||||
## When to make a utility API
|
||||
|
||||
> TODO
|
||||
This section describes how to make a Utility API from scratch, or to add configurability and inputs to an existing one. If you are instead interested in migrating an existing Utility API from the old frontend system, check out [the migrating section](./05-migrating.md).
|
||||
|
||||
## Creating the Utility API contract
|
||||
|
||||
@@ -24,7 +22,6 @@ import { createApiRef } from '@backstage/frontend-plugin-api';
|
||||
export interface WorkApi {
|
||||
/**
|
||||
* Performs some work.
|
||||
* @public
|
||||
*/
|
||||
doWork(): Promise<void>;
|
||||
}
|
||||
@@ -40,6 +37,8 @@ export const workApiRef = createApiRef<WorkApi>({
|
||||
|
||||
Both of these are properly exported publicly from the package, so that consumers can reach them.
|
||||
|
||||
## Providing an extension through your plugin
|
||||
|
||||
The plugin itself now wants to provide this API and its default implementation, in the form of an API extension. Doing so means that when users install the Example plugin, an instance of the Work utility API will also be automatically available in their apps - both to the Example plugin itself, and to others. We do this in the main plugin package, not the `-react` package.
|
||||
|
||||
```tsx title="in @internal/plugin-example"
|
||||
@@ -65,7 +64,9 @@ const exampleWorkApi = createApiExtension({
|
||||
factory: createApiFactory({
|
||||
api: workApiRef,
|
||||
deps: { storageApi: storageApiRef },
|
||||
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
|
||||
factory: ({ storageApi }) => {
|
||||
return new WorkImpl({ storageApi });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -83,8 +84,60 @@ For illustration we make a skeleton implementation class and the API extension a
|
||||
|
||||
The code also illustrates how the API factory declares a dependency on another utility API - the core storage API in this case. An instance of that utility API is then provided to the factory function.
|
||||
|
||||
The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. You can now use this ID to refer to the API in app-config and elsewhere.
|
||||
The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. Check out [the naming patterns doc](../architecture/08-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere.
|
||||
|
||||
## Adding configurability
|
||||
|
||||
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's make the required additions to our original work example API.
|
||||
|
||||
```tsx title="in @internal/plugin-example"
|
||||
/* highlight-add-next-line */
|
||||
import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const exampleWorkApi = createApiExtension({
|
||||
/* highlight-add-start */
|
||||
api: workApiRef,
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
goSlow: z.boolean().default(false),
|
||||
}),
|
||||
),
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
factory: createApiFactory({
|
||||
/* highlight-add-next-line */
|
||||
factory: ({ config }) => createApiFactory({
|
||||
api: workApiRef,
|
||||
deps: { storageApi: storageApiRef },
|
||||
factory: ({ storageApi }) => {
|
||||
/* highlight-add-start */
|
||||
if (config.goSlow) {
|
||||
/* ... */
|
||||
}
|
||||
/* highlight-add-end */
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
We wanted users to be able to set a `goSlow` extension config parameter for our API instances. So we passed in a `configSchema` to `createApiExtension` which matches that interface. This example builds it using [the zod library](https://zod.dev/). The actual extension config values will then be passed in a type safe manner in to the `factory` which is now a callback, wherein we can do what we wish with them. When changing to the callback form, we also had to add a top level `api: workApiRef` under `createApiExtension`.
|
||||
|
||||
Note that the expression "extension config" as used here, is _not_ the same thing as the `configApi` which gives you access to the full app-config. The extension config discussed here is instead the particular configuration settings given to your utility API instance. This is discussed more [in the Configuring section](./04-configuring.md).
|
||||
|
||||
Note also that the extension config schema contained a default value fo the `goSlow` field. This is an important consideration. You want users of your API to be able to get maximum value out of it, without having to dive deep into how to configure it. For that reason you generally want to provide as many sane defaults as possible, while letting users override them rarely but with purpose, only when called for. If you have an extension config schema without defaults, the framework will refuse to instantiate the utility API on startup unless the user had configured those values explicitly. Since it had a default value, the TypeScript code and interfaces also don't have to defensively allow `undefined` - we know that it'll have either the default value or an overridden value when we start consuming the extension config data.
|
||||
|
||||
## Adding inputs
|
||||
|
||||
Inputs are added to Utility APIs in the same way as other extension types:
|
||||
|
||||
- Declaring a set of `inputs` on your extension
|
||||
- If needed, create custom extension data types to be used in those inputs
|
||||
- If needed, export an extension creator function for creating that particular attachment type
|
||||
|
||||
This is a power use case and not very commonly used.
|
||||
|
||||
<!-- TODO: link to main article -->
|
||||
|
||||
## Next steps
|
||||
|
||||
See [the Consuming section](./03-consuming.md) to see how to consume this new utility API in various ways. If you wish to learn how to add configurability and inputs to it, check out [the Configuring section](./04-configuring.md).
|
||||
See [the Consuming section](./03-consuming.md) to see how to consume this new utility API in various ways. If you wish to configure and add inputs to it, check out [the Configuring section](./04-configuring.md).
|
||||
|
||||
@@ -6,76 +6,69 @@ sidebar_label: Configuring
|
||||
description: Configuring, extending, and overriding utility APIs
|
||||
---
|
||||
|
||||
Utility APIs are extensions and can therefore optionally be amended with configurability, as well as inputs that other extensions attach themselves to. This section describes how to add those capabilities to your own utility APIs, and how to make use of them as a consumer of such utility APIs.
|
||||
|
||||
## Adding configurability
|
||||
|
||||
Here we will describe how to amend a utility API with the capability of being configured in app-config. You do this by giving a config schema to your API extension factory function.
|
||||
|
||||
Let's make the required additions to [our original work example](./02-creating.md) API.
|
||||
|
||||
```tsx title="in @internal/plugin-example"
|
||||
import {
|
||||
createApiExtension,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
/* highlight-add-next-line */
|
||||
createSchemaFromZod,
|
||||
storageApiRef,
|
||||
StorageApi,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { WorkApi, workApiRef } from '@internal/plugin-example-react';
|
||||
|
||||
/* highlight-add-start */
|
||||
interface WorkApiConfig {
|
||||
goSlow: boolean;
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
const workApi = createApiExtension({
|
||||
/* highlight-add-start */
|
||||
api: workApiRef,
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
goSlow: z.boolean().default(false),
|
||||
}),
|
||||
),
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
factory: createApiFactory({
|
||||
/* highlight-add-next-line */
|
||||
factory: ({ config }) => createApiFactory({
|
||||
api: workApiRef,
|
||||
deps: { storageApi: storageApiRef },
|
||||
factory: ({ storageApi }) => {
|
||||
/* highlight-add-start */
|
||||
if (config.goSlow) {
|
||||
/* ... */
|
||||
}
|
||||
/* highlight-add-end */
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
We wanted users to be able to configure a `goSlow` parameter for our API instances. So we added another interface type for holding our various options, and passed in a `configSchema` to `createApiExtension` which matches that interface. This example builds it using [the zod library](https://zod.dev/). The actual config values will then be passed in to the `factory` which is now a callback, wherein we can do what we wish with them. When changing to the callback form, we also had to add a top level `api: workApiRef` under `createApiExtension`.
|
||||
|
||||
Note that while we use the word "config" here, it's _not_ the same thing as the `configApi` which gives you access to the full app-config. The config discussed here is instead the particular configuration settings given to your utility API instance. This is discussed more [in the section below](#configuring).
|
||||
|
||||
Note also that the config schema contained a default value fo the `goSlow` field. This is an important consideration. You want users of your API to be able to make maximum use of it, without having to dive deep into how to configure it. For that reason you generally want to provide as many sane defaults as possible, while letting users override them rarely but with purpose, only when called for. If you have a config schema without defaults, the framework will refuse to instantiate the utility API on startup unless the user had configured those values explicitly. Since it had a default value, the TypeScript code and interfaces also don't have to defensively allow `undefined` - we know that it'll have either the default value or an overridden value when we start consuming the config data.
|
||||
Utility APIs are extensions and can therefore optionally be amended with configurability, as well as inputs that other extensions attach themselves to. This section describes how to make use of that as a consumer of such utility APIs.
|
||||
|
||||
## Configuring
|
||||
|
||||
> TODO
|
||||
To configure your Utility API extension, first you'll need to know its ID. That ID is formed from the API ref ID; check [the naming patterns docs](../architecture/08-naming-patterns.md) for details.
|
||||
|
||||
## Adding extension inputs
|
||||
Our example work API from [the creating section](./02-creating.md) would have the ID `api:plugin.example.work`. You configure it and all other extensions under the `app.extensions` section of your app-config.
|
||||
|
||||
> TODO
|
||||
```yaml title="in e.g. app-config.yaml or app-config.production.yaml"
|
||||
app:
|
||||
extensions:
|
||||
- api:plugin.example.work:
|
||||
config:
|
||||
goSlow: false
|
||||
- # ... other extensions
|
||||
```
|
||||
|
||||
## Adding data to extension inputs
|
||||
It's important to note that the `extensions` are a list (mind the initial `-`), and that the `api:plugin.example.work` entry is an object such that the `config` key needs to be indented below it. If you do not get those two pieces right, the application may not start up correctly.
|
||||
|
||||
> TODO
|
||||
The extension config schema will tell you what parameters it supports. Here we override the `goSlow` extension config value, which replaces the default.
|
||||
|
||||
## Replacing a utility API implementation
|
||||
## Attaching extensions to inputs
|
||||
|
||||
> TODO
|
||||
Like with other extension types, you add input attachments to a Utility API by declaring the `attachTo` section of that attachment to point to the Utility APIs ID and input name.
|
||||
|
||||
Well written input-enabled extension often have extension creator functions that help you make such attachments. Those functions typically set the `attachTo` section correctly on your behalf so that you don't have to figure them out.
|
||||
|
||||
## Replacing a Utility API implementation
|
||||
|
||||
Like with other extension types, you replace Utility APIs with your own custom implementation using [extension overrides](../architecture/05-extension-overrides.md).
|
||||
|
||||
```tsx title="in your app"
|
||||
/* highlight-add-start */
|
||||
import { createExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
|
||||
class CustomWorkImpl implements WorkApi {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
const myOverrides = createExtensionOverrides({
|
||||
extensions: [
|
||||
createApiExtension({
|
||||
api: workApiRef,
|
||||
factory: () =>
|
||||
createApiFactory({
|
||||
api: workApiRef,
|
||||
factory: () => new CustomWorkImpl(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
// Remember to pass the overrides to your createApp
|
||||
export default createApp({
|
||||
features: [
|
||||
// ... other features
|
||||
/* highlight-add-next-line */
|
||||
myOverrides,
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
In this example the overriding extension is kept minimal, but just like any other extension it can also have `deps`, configurability, and inputs. Check out [the Creating section](./02-creating.md) for more details about that.
|
||||
|
||||
When you create a replacement extension, in general you may want to mimic its extension config schema or input shapes where applicable. This makes it an easier thing to slot in to an app, since it'll be responding to extensibility the same way as the original one did.
|
||||
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
import { workApiRef } from '@internal/plugin-example-react';
|
||||
import { WorkImpl } from './WorkImpl';
|
||||
|
||||
const workApi = createApiFactory({
|
||||
const exampleWorkApi = createApiFactory({
|
||||
api: workApiRef,
|
||||
deps: { storageApi: storageApiRef },
|
||||
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
|
||||
@@ -81,7 +81,7 @@ const workApi = createApiFactory({
|
||||
/** @public */
|
||||
export const catalogPlugin = createPlugin({
|
||||
id: 'example',
|
||||
apis: [workApi],
|
||||
apis: [exampleWorkApi],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,6 +92,8 @@ The major changes we'll make are
|
||||
- Change to the new version of `createPlugin` which exports this extension
|
||||
- Change the plugin export to be the default instead
|
||||
|
||||
The end result, after simplifying imports and cleaning up a bit, might look like this:
|
||||
|
||||
```tsx title="in @internal/plugin-example"
|
||||
import {
|
||||
storageApiRef,
|
||||
@@ -102,7 +104,7 @@ import {
|
||||
import { workApiRef } from '@internal/plugin-example-react';
|
||||
import { WorkImpl } from './WorkImpl';
|
||||
|
||||
const workApi = createApiExtension({
|
||||
const exampleWorkApi = createApiExtension({
|
||||
factory: createApiFactory({
|
||||
api: workApiRef,
|
||||
deps: { storageApi: storageApiRef },
|
||||
@@ -113,10 +115,10 @@ const workApi = createApiExtension({
|
||||
/** @public */
|
||||
export default createPlugin({
|
||||
id: 'example',
|
||||
extensions: [workApi],
|
||||
extensions: [exampleWorkApi],
|
||||
});
|
||||
```
|
||||
|
||||
## Further work
|
||||
|
||||
Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](./04-configuring.md) to your API, if that makes sense for your current application.
|
||||
Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](./02-creating.md) to your API, if that makes sense for your current application.
|
||||
|
||||
Reference in New Issue
Block a user