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

An API portal needs four things: the api-docs plugin to render contracts, OpenAPI specs checked into each repo, catalog ingestion so entities are discovered, and a search backend that scales past the in-memory default.

Prerequisites for an API portal The api-docs plugin, committed OpenAPI specs, catalog ingestion, and a scalable search backend enable API discovery. api-docs plugin OpenAPI in repos catalog ingestion search backend Discoverable APIs searchable + owned
The portal renders the contract it is given — it never generates one, so a committed spec per repo is non-negotiable.
  • 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

The model is two linked entities: an API entity holds the contract, a Component declares that it providesApis, and that bidirectional relation is what powers the dependency graph and interactive docs. The diagram shows the linkage.

API entity linked to its owning component A Component providesApis to an API entity holding an OpenAPI contract, which renders as interactive docs. Component providesApis API entity $text contract Interactive docs try-it-out bidirectional relation
The API kind is deliberately distinct from Component — one holds the contract, the other declares it provides it.

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]

Confirm three properties: the API entity resolved with its definition inlined, the provides/consumes relation is bidirectional, and the API is returned by search.

Three API-discovery validation checks The definition inlined, a bidirectional relation, and a search hit confirm the API is discoverable. Definition inlined spec resolved Relation both ways provides / consumes Search hit results ≥ 1
A definition that resolves but returns nothing from search usually means the API kind is missing from the search rules.
# 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

Maintenance keeps the portal trustworthy: generate specs in CI so they never drift, deprecate rather than delete retired APIs, gate sensitive contracts behind RBAC, and rebuild the search index off-peak.

API portal maintenance CI-generated specs, deprecation over deletion, RBAC gating, and off-peak index rebuilds keep the portal trustworthy. CI-generate spec fail on drift Deprecate not delete RBAC gate sensitive contracts Rebuild index off-peak
Deprecating instead of deleting keeps the dependency graph resolvable while consumers migrate off a retired API.
  • 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

The API-discovery pitfalls split into source-of-truth failures (wiki docs, stale specs, missing owners) and infrastructure failures (broken $text refs, search exhausted at scale). The diagram groups them so the fix is either a golden-path requirement or a backend change.

API-discovery pitfalls by class Wiki docs, stale specs, and missing owners are source-of-truth failures; broken refs and search scale are infrastructure failures. Source of truth wiki docs stale + no owner → golden-path requires Infrastructure broken $text ref Lunr exhausted → CI check + Postgres
Requiring an API entity as a golden-path output eliminates the wiki-drift and missing-owner pitfalls at the source.
  • 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.