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.0 with catalog ingestion configured for your VCS provider.
  • @backstage/plugin-api-docs >= 0.11.0 registered in the app for interactive rendering.
  • An OpenAPI 3.0 or 3.1 document checked into the service repository.
  • A resolvable owner — the Group entity must already exist in the catalog.
  • Catalog rules that allow the API kind (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.

# 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.