Publishing OpenAPI Specs to the Service Catalog
Registering an API as a first-class catalog entity is what turns a buried openapi.yaml into discoverable, owned, interactively-rendered documentation. This how-to covers the API entity kind, the $text placeholder that inlines a spec at ingestion, and the relations that link an API to the service that provides it. It implements the contract-publishing step of Internal API Portals & Discovery within Developer Experience & Self-Service Platforms.
Prerequisites
- Backstage
>= 1.20.0with catalog ingestion configured for your VCS provider. @backstage/plugin-api-docs>= 0.11.0registered in the app for interactive rendering.- An OpenAPI 3.0 or 3.1 document checked into the service repository.
- A resolvable
owner— theGroupentity must already exist in the catalog. - Catalog
rulesthat allow theAPIkind (see step 4).
Exact Configuration
1. Add the API entity to catalog-info.yaml
# catalog-info.yaml
# Requires Backstage >= 1.20.0
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payments-api
description: Synchronous payment authorization API
tags: [rest, payments]
spec:
type: openapi
lifecycle: production
owner: group:payments-platform
system: payments
definition:
$text: ./openapi.yaml
The $text placeholder tells the catalog to read the referenced file and inline its contents into spec.definition at ingestion time. The path is resolved relative to catalog-info.yaml, so co-locate the spec in the same repository.
2. Link the providing component
# catalog-info.yaml (Component for the same service)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-service
spec:
type: service
lifecycle: production
owner: group:payments-platform
providesApis:
- payments-api
consumesApis:
- fraud-check-api
providesApis and consumesApis create bidirectional relations, so the API page lists its providers and the component page lists its interfaces. These relations follow the same modeling discipline as your catalog integration patterns.
3. Validate the spec in CI before merge
# .github/workflows/api-validate.yml
# Requires Backstage >= 1.20.0
name: Validate API Entity
on:
pull_request:
paths: ['**/openapi.yaml', '**/catalog-info.yaml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint the OpenAPI document
run: npx @redocly/cli@^1.0.0 lint openapi.yaml
- name: Validate the catalog entity
env:
CATALOG_SERVICE_TOKEN: ${{ secrets.CATALOG_SERVICE_TOKEN }}
run: npx @backstage/cli@^0.27.0 catalog validate --path ./catalog-info.yaml
4. Allow the API kind in ingestion rules
# app-config.yaml
# Requires Backstage >= 1.20.0
catalog:
rules:
- allow: [Component, API, System, Group, Resource]
locations:
- type: url
target: https://github.com/${GITHUB_ORG}/payments-service/blob/main/catalog-info.yaml
Validation
# Requires Backstage >= 1.20.0
# 1. The API entity exists and inlined its definition
curl -s "${CATALOG_BACKEND_URL}/api/catalog/entities/by-name/api/default/payments-api" \
-H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" \
| jq '.spec.definition | length > 100'
# Expected: true (the spec text is inlined, not a path)
# 2. The provides relation resolved on the component
curl -s "${CATALOG_BACKEND_URL}/api/catalog/entities/by-name/component/default/payments-service" \
-H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" \
| jq '[.relations[] | select(.type=="providesApi") | .targetRef]'
# Expected: ["api:default/payments-api"]
# 3. Local validation passes before pushing
npx @backstage/cli@^0.27.0 catalog validate --path ./catalog-info.yaml
# Expected: "Validated 2 entities" with no errors
Edge Cases & Troubleshooting
| Symptom | Root Cause | Resolution |
|---|---|---|
spec.definition is the literal path, not the spec |
$text not resolved — file unreachable at ingestion |
Confirm the relative path is correct and the catalog reader has access to the repo |
| API page renders blank | Spec is invalid OpenAPI | Run redocly lint; the renderer silently fails on malformed documents |
| Owner shows as unknown | group:payments-platform not ingested |
Register the Group entity first; ordering matters on a cold catalog |
providesApi relation missing |
API name typo in providesApis |
The value must match the API entity metadata.name exactly |
| API kind rejected at ingestion | catalog.rules omits API |
Add API to the allow list and refresh the location |
Frequently Asked Questions
Can I reference a spec by URL instead of a relative path?
Yes — $text accepts an absolute URL, useful when the spec is published to an artifact store. Prefer the relative path when the spec lives in the same repo, because it stays versioned with the code and CI can fail on drift. A remote URL needs the catalog reader authorized for that host.
Does inlining a large spec bloat the catalog database?
A typical OpenAPI document is tens of kilobytes and inlines without issue. For very large specs, keep ownership and discovery in the catalog while linking out to a hosted artifact for the full document via spec.definition.$text pointing at a URL, so the entity row stays lean.