This file describes the various style and design conventions that are used in the Backstage main repository. While you may choose to use these conventions in your own Backstage projects, it is not required.
Our TypeScript style is inspired by the style guidelines of the TypeScript implementation itself.
- Use PascalCase for type names.
- Do not use
I
as a prefix for interface names. - Use PascalCase for
enum
values. - Use
camelCase
for function names. - Use
camelCase
for property names and local variables. - Do not use
_
as a prefix for private properties. - Use whole words in names when possible.
- Give type parameters names prefixed with
T
, for exampleRequest<TBody>
.
- Use
undefined
. Do not usenull
. - Prefer
for..of
over.forEach
. - Do not introduce new types/values to the global namespace.
- Shared types should be defined in
types.ts
. - Keep
index.ts
free from implementation, it should only contain re-exports. - If a file has a single or a main export, name the file after the export.
- Rely on
@backstage/errors
for custom error types.throw new NotFoundError(`Could not find resource with id '${id}'`);
- Check error types by comparing the name
if (error.name === 'NotFoundError') { // ... }
- Use
ResponseError
to convertfetch
error responses.if (!res.ok) { throw await ResponseError.fromResponse(res); }
This section describes guidelines for designing public APIs. It can also be applied to internal implementations, but it is less necessary.
-
Keep SOLID principles in mind.
-
Be mindful of the number of top-level exports of each package, strive to keep it low.
-
Prioritize consistency over correctness, stick to existing patterns within the same class/file/folder/package.
-
Consume interfaces rather than concrete implementations.
-
Prefer classes for encapsulating functionality and implementing interfaces.
-
Suffix the name of concrete implementations with the name of the implemented interface. Use a prefix that describes the behavior of the implementation, or use a
Default
prefix.interface ImageLoader { ... } // interface for loading images class DefaultImageLoader implements ImageLoader { /* loads an image */ } class CachingImageLoader implements ImageLoader { /* caches loaded images */ } class ResizingImageLoader implements ImageLoader { /* resizes loaded images */ }
-
Keep constructors private, prefer static factory methods for creating instances.
class DefaultImageLoader implements ImageLoader { // Use `create` for the main way to create an instance. static create(options?: ImageLoaderOptions) { /* ... */ } // If there are multiple different types of instances that can be // created, suffix the create method. static createWithCaching(options?: ImageLoaderOptions) { /* ... */ } // If the instantiation process is based on a specific value, use `from*`. // The most common example of this is reading from configuration. // Use a second parameter in case additional options are needed. static fromConfig(config: Config, deps: { logger: Logger }) { /* ... */ } // Other types of values can work too static fromUrl(url: URL) { /* ... */ } // In order to make a private constructor available for testing you can use a // static factory marked as `@internal`, which will not show up in the public API. /** @internal */ static forTesting(internalOptions?: { ... }) { return new DefaultImageLoader(internalOptions); } private constructor(/* ... */) { /* ... */ } }
-
Prefer common prefixes over suffixes when naming constants.
// May be tempting to use `GITHUB_WIDGET_LABEL` instead. const WIDGET_LABEL_GITHUB = 'github'; const WIDGET_LABEL_GITLAB = 'gitlab'; const WIDGET_LABEL_BITBUCKET = 'bitbucket';
-
When a type relates directly to other symbols, use the name of those as prefix for the type.
// Always prefix a prop type with the name of the component. function MyComponent(props: MyComponentProps) {} // Option types should be prefixed with the name of the operation. function upgradeWidget(options: UpgradeWidgetOptions) {} function activateWidget(options: ActivateWidgetOptions) {} // An exception to this are create methods, where the name of the thing // being created may be used as the prefix instead. function createWidget(options: WidgetOptions) {} // In this case the related names for request types are `ReportsApi` and // the method name. If there is a low risk of conflict we can keep them // short by only prefixing with the method name, but if there is a higher // risk of conflict then we would want to use the full prefix instead, while // omitting redundant parts, i.e. `ReportsApiUploadRequest. interface ReportsApi { uploadReports(request: UploadReportsRequest): Promise<void>; deleteReport(request: DeleteReportRequest): Promise<void>; }
-
When there is a significant number of arguments to a function or method, prefer to use a single options object as the argument, rather than many positional arguments.
// Bad function createWidget(id: string, name: string, width: number) {} // Good function createWidget(options: CreateWidgetOptions) {}
-
Avoid arrays as return types; prefer response objects.
interface UserApi { // Bad // Can only return Users without signaling additional information such as pagination. listUsers(): Promise<User[]>; // Good // Easy to evolve with additional fields. listUsers(): Promise<ListUsersResponse>; }
We use API Extractor to generate our documentation, which in turn uses TSDoc to parse our doc comments.
The doc comments are of the good old /** ... */
format, with tags of the format @<tag>
used to mark various things. The TSDoc website has a good index of all available tags.
There are a few things to pay attention to, in order to make the documentation show up in a nice way on the website...
API documenter will not recognize arrow functions as functions, but rather as a constant that shows up in the list of exported variables. By declaring functions using the function
keyword, they will show up in the list of functions. They will also get a much nicer documentation page for the individual function that shows information about parameters and return types.
This also extends to React components, since API documenter doesn't have any special handling of those. By always defining exported React components using the function
keyword, we make them show up among the list of functions in the API reference, where they are then easily discoverable through the (props)
argument (which you should be sure to include!).
/**
* Properties for {@link ErrorPanel}.
*/
export interface ErrorPanelProps {
...
}
/**
* Renders a warning panel as the effect of an error.
*/
export function ErrorPanel(props: ErrorPanelProps) {
...
}
If the parameters of a function are destructed in the parameter list, they will show up in the documentation like this:
Instead prefer to use a single parameter variable and then move the destructuring into the function body, which will look much nicer:
Also be sure to check that the type used by the parameter is exported, as it otherwise won't be discoverable through documentation.
The API reference has an index of exported symbols for each package, which uses a short description, while clicking through to the page for a symbol shows the full description. By default all descriptions are considered "short", and you have to manually add a divider where the description should be cut off using the @remarks
tag.
/**
* This function helps you create a thing.
*
* @remarks
*
* Here is a much longer and more elaborate description of how the
* creation of a thing works, which is way too long to fit on the index page.
*/
function createTheThing() {}
When using the @param
tag to document a parameter it will show up in the Parameters section:
Be sure to include a -
after the parameter name as well as required by TSDoc, or you'll get a warning in the API report.
/**
* Generates a PluginCacheManager for consumption by plugins.
*
* @param pluginId - The plugin that the cache manager should be created for. Plugin names should be unique.
*/
forPlugin(pluginId: string): PluginCacheManager {
...
}
Not all types are detected and referenced on the documentation page. Most notably variables, and therefore ApiRef
s, will not have clickable links to other symbols that they reference. We instead fill in this missing information using inline {@link ...}
tags in the description:
/**
* {@link ApiRef} for the {@link DiscoveryApi}.
*/
export const discoveryApiRef: ApiRef<DiscoveryApi> = createApiRef(...);