Internal API Portals & Discovery

Internal APIs are only reusable if engineers can find them, read their contracts, and trust their ownership — which means surfacing every service interface through the catalog rather than scattered wikis and Slack threads.

When an API is undiscoverable, teams rebuild it. An internal API portal turns the service catalog into the single index of every HTTP, gRPC, and async interface in the organization, each one backed by a machine-readable contract and a named owner. This capability covers modeling APIs as first-class catalog entities, attaching OpenAPI definitions so the portal can render interactive docs, and wiring search so an engineer can go from “does this exist?” to a live contract in seconds. It builds directly on your catalog integration patterns and complements the golden paths that govern how new services expose APIs in the first place.

API discovery flow through the catalog Service repositories publish OpenAPI specs that become API entities, linked to owning components and rendered as searchable interactive docs. Service Repo openapi.yaml API Entity kind: API Component providesApis Catalog Index $text search API Docs try-it-out
OpenAPI specs become API entities, link to their owning components, and surface as searchable interactive documentation.

Prerequisites & Environment Baseline

  • Backstage >= 1.20.0 with the catalog and @backstage/plugin-api-docs >= 0.11.0 installed in the app. The API entity kind and the OpenAPI renderer ship with these.
  • OpenAPI 3.0/3.1 specs checked into each service repository. The portal renders the contract it is given; it does not generate one.
  • Catalog ingestion already configured for your VCS provider so new catalog-info.yaml files are discovered automatically — see Catalog Integration Patterns.
  • A search engine backend for non-trivial scale. The default in-memory Lunr index works for a few hundred entities; Postgres or Elasticsearch is required beyond that, as covered in the Backstage Architecture Deep Dive.
# Confirm the api-docs plugin is installed
# Requires Backstage >= 1.20.0
grep -r "@backstage/plugin-api-docs" packages/app/package.json

# Confirm at least one API entity is already ingested
curl -s "${CATALOG_BACKEND_URL}/api/catalog/entities?filter=kind=api" \
  -H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" | jq 'length'

Step-by-Step Configuration & Plugin Architecture

1. Model the API as a catalog entity

The API kind is distinct from Component. A component provides or consumes APIs; the API entity holds the contract. Full schema details are in Publishing OpenAPI Specs to the Service Catalog.

# catalog-info.yaml
# Requires Backstage >= 1.20.0
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: payments-api
  description: Synchronous payment authorization API
spec:
  type: openapi
  lifecycle: production
  owner: group:payments-platform
  definition:
    $text: ./openapi.yaml   # relative to this file, resolved at ingestion

Relations power the dependency graph and let an engineer jump from a service to its interfaces and back.

# 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

3. Render interactive docs

With @backstage/plugin-api-docs registered, the API entity page renders the OpenAPI definition with a Swagger-style explorer. No extra wiring is needed beyond the route in App.tsx.

// packages/app/src/App.tsx
// Requires @backstage/plugin-api-docs >= 0.11.0
import { ApiExplorerPage } from '@backstage/plugin-api-docs';

// Within <FlatRoutes>:
// <Route path="/api-docs" element={<ApiExplorerPage />} />

4. Make APIs searchable

The complete portal-building walkthrough — search filters, an API explorer landing page, and ownership facets — is in Building an Internal API Discovery Portal. Index API entities so their paths and descriptions are full-text searchable.

# app-config.yaml
# Requires Backstage >= 1.20.0
search:
  pg:
    highlightOptions:
      useHighlight: true
catalog:
  rules:
    - allow: [Component, API, System, Group]

Validation & Health Checks

# Requires Backstage >= 1.20.0
# 1. Verify the API entity resolved with its definition inlined
curl -s "${CATALOG_BACKEND_URL}/api/catalog/entities/by-name/api/default/payments-api" \
  -H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" | jq '.spec.definition | length > 0'
# Expected: true

# 2. Verify the provides/consumes relation is bidirectional
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")'
# Expected: a relation targeting api:default/payments-api

# 3. Verify the API is searchable
curl -s "${SEARCH_BACKEND_URL}/api/search/query?term=payments&types[0]=software-catalog" \
  -H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" | jq '.results | length'
# Expected: >= 1

Maintenance & Lifecycle Management

  • Spec freshness: treat the OpenAPI file as a build artifact. Generate it from code in CI and fail the build on drift so the catalog never renders a stale contract.
  • Deprecation: move retired APIs to lifecycle: deprecated rather than deleting them, so consumers see the warning banner and the dependency graph still resolves while they migrate.
  • Upgrade path: @backstage/plugin-api-docs tracks the catalog model closely; review its changelog on each Backstage minor bump, particularly for OpenAPI 3.1 rendering changes.
  • Access control: API contracts can leak internal topology. Gate the API explorer behind your RBAC setup and record reads through audit logging where contracts are sensitive.
  • Index rebuilds: schedule full search re-indexing off-peak; partial updates can miss entities deleted between scheduled collations.

Common Pitfalls & Mitigation Strategies

  • APIs documented in wikis, not the catalog — Root cause: no enforced source of truth. Fix: make an API entity a required output of the golden-path template so every new service registers its interface automatically.
  • Stale specs — Root cause: hand-maintained OpenAPI files. Fix: generate specs from code in CI and validate the committed file matches on every PR.
  • Broken $text references — Root cause: wrong relative path or unauthenticated raw URL. Fix: keep the spec beside catalog-info.yaml and validate resolution in CI before merge.
  • Missing ownership — Root cause: API entities without a resolvable owner. Fix: enforce a Group owner at ingestion and reject orphaned APIs.
  • Search returns nothing at scale — Root cause: in-memory Lunr index exhausted. Fix: move to the Postgres or Elasticsearch search backend before crossing a few hundred entities.

Frequently Asked Questions

Should every internal API become a catalog entity, even internal-only RPCs?

Yes, if other teams can call it. The value of the portal is completeness — an interface that exists but is undiscoverable gets reimplemented. Reserve the exception for truly private, single-team endpoints that are never meant for reuse, and even then a catalog entry with lifecycle: experimental is cheap insurance.

How do we handle gRPC or async (event) APIs that are not OpenAPI?

The API entity supports multiple spec.type values, including grpc and asyncapi. Store the .proto or AsyncAPI document via the same $text mechanism. The api-docs plugin renders OpenAPI interactively; other types render as formatted source, which still centralizes discovery and ownership.

Where should the OpenAPI spec live — in the service repo or a central one?

In the service repository, beside catalog-info.yaml. Co-locating the contract with the code keeps it versioned with the implementation and lets CI fail the build when they diverge. A central spec repository reintroduces the drift problem the catalog is meant to eliminate.