Building an Internal API Discovery Portal

A discovery portal is the difference between “we have a catalog” and “an engineer can find the right API in ten seconds.” This how-to assembles the API explorer, ownership and lifecycle facets, and a search backend that scales past the default in-memory index — turning registered API entities into a browsable, filterable directory. It is the consumer-facing payoff of Internal API Portals & Discovery within Developer Experience & Self-Service Platforms, building on entities published per Publishing OpenAPI Specs to the Service Catalog.

A discovery portal needs four things in place: API entities already ingested, the api-docs and search plugins installed, a Postgres store for the index at scale, and app access to wire routes and the search backend.

Prerequisites for an API discovery portal Ingested API entities, the api-docs and search plugins, a Postgres store, and app access make the portal buildable. API entities api-docs + search Postgres store app access Portal buildable browsable APIs
The Postgres search module is the prerequisite that matters at scale — the default in-memory index stalls past a few hundred entities.
  • Backstage >= 1.20.0 with API entities already ingested.
  • @backstage/plugin-api-docs >= 0.11.0 and @backstage/plugin-search >= 1.4.0 with @backstage/plugin-search-backend-module-pg for a Postgres-backed index.
  • PostgreSQL >= 14 as the catalog and search store at non-trivial scale.
  • App access to App.tsx, apis.ts, and the search backend module wiring.
  • Optional: your RBAC setup configured if API contracts must be gated.

Exact Configuration

The portal is assembled from four pieces: an explorer route and a sidebar entry for discoverability, a Postgres-backed search backend for scale, and facet filters that narrow by type, lifecycle, and owner. The diagram shows how they fit.

Discovery portal assembly An explorer route plus sidebar entry, a Postgres search backend, and facet filters compose the discovery portal. Explorer route + sidebar PG search backend catalog collator Index + collate highlight Facet filters type · lifecycle
Facet filters keyed on exact spec field paths are what turn a flat list into a directory an engineer can narrow fast.

1. Mount the API explorer route

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

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

2. Add a sidebar entry for discoverability

// packages/app/src/components/Root/Root.tsx
// Requires @backstage/core-components >= 0.14.0
import ExtensionIcon from '@material-ui/icons/Extension';
import { SidebarItem } from '@backstage/core-components';

// Within <SidebarGroup>:
// <SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />

3. Wire the Postgres search backend

// packages/backend/src/index.ts
// Requires @backstage/plugin-search-backend-module-pg >= 0.5.0
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();
backend.add(import('@backstage/plugin-search-backend'));
backend.add(import('@backstage/plugin-search-backend-module-pg'));
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.start();

4. Index API entities and schedule collation

# app-config.yaml
# Requires Backstage >= 1.20.0
search:
  pg:
    highlightOptions:
      useHighlight: true
backend:
  database:
    client: pg
    connection:
      host: ${PG_HOST}
      user: ${PG_USER}
      password: ${PG_PASSWORD}
catalog:
  rules:
    - allow: [Component, API, System, Group]

5. Add a facet filter for type, lifecycle, and owner

The API explorer ships with column filters out of the box. For a richer landing experience, embed a search filter panel scoped to API entities. The card-composition technique mirrors Building a Custom Entity Page Card in Backstage.

// packages/app/src/components/apiSearch/ApiSearchFilters.tsx
// Requires @backstage/plugin-search-react >= 1.7.0
import { SearchFilter } from '@backstage/plugin-search-react';

export const ApiSearchFilters = () => (
  <>
    <SearchFilter.Select name="spec.type" label="Type"
      values={['openapi', 'grpc', 'asyncapi']} />
    <SearchFilter.Select name="spec.lifecycle" label="Lifecycle"
      values={['experimental', 'production', 'deprecated']} />
  </>
);

Confirm three signals: API entities exist in the catalog, search returns API results with highlighting, and the explorer route responds. Each verifies a different layer of the portal.

Three discovery-portal validation checks API entities present, search returns API results, and the explorer route responds 200. Entities present catalog query Search returns APIs with highlight Explorer 200 route responds
Entities present but absent from search almost always means the catalog search collator was never registered.
# Requires Backstage >= 1.20.0
# 1. API entities are present in the catalog
curl -s "${CATALOG_BACKEND_URL}/api/catalog/entities?filter=kind=api" \
  -H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" | jq 'length'
# Expected: matches the number of registered APIs

# 2. Search returns API results with highlighting
curl -s "${SEARCH_BACKEND_URL}/api/search/query?term=payments" \
  -H "Authorization: Bearer ${CATALOG_SERVICE_TOKEN}" \
  | jq '[.results[] | select(.document.kind=="API")] | length'
# Expected: >= 1

# 3. The explorer route responds
curl -s -o /dev/null -w "%{http_code}\n" "${PORTAL_URL}/api-docs"
# Expected: 200

The discovery-portal pitfalls split into indexing issues (no results, slow search) and presentation issues (blank docs, dead facets, deprecated clutter). The diagram groups them so the fix is either a backend/collator change or a UI/filter change.

Discovery-portal pitfalls by class No results and slow search are indexing issues; blank docs, dead facets, and deprecated clutter are presentation issues. Indexing no results slow at scale → collator + Postgres Presentation blank docs dead facet / clutter → exact path + default
Default the explorer to hide deprecated APIs and let users opt in — clutter is the quiet killer of discoverability.
Symptom Root Cause Resolution
Search returns no APIs Catalog collator not indexing the API kind Confirm the catalog search module is registered and re-run collation
Explorer lists APIs but docs are blank OpenAPI spec invalid or $text unresolved Validate the spec and the entity per the publishing how-to
Filters show no facet values Filter name does not match entity field Use the exact spec field path, e.g. spec.lifecycle
Search slow above ~500 entities In-memory Lunr index still active Confirm the Postgres search module loaded and Lunr is not also registered
Deprecated APIs clutter results No lifecycle filter applied Default the explorer to lifecycle != deprecated and let users opt in

Frequently Asked Questions

Do we need Elasticsearch, or is Postgres search enough?

Postgres search comfortably handles thousands of entities with highlighting, and it avoids running a second datastore. Reach for Elasticsearch only when you need advanced relevance tuning, fuzzy matching, or tens of thousands of documents — for most internal portals, the Postgres module is the right default.

How do we keep the portal from exposing sensitive API contracts?

Gate the explorer route and the search results behind the permission framework so only authorized groups see restricted APIs, and tag sensitive entities so they are filtered server-side rather than hidden only in the UI. Pair this with your role-based access control setup to enforce it at the API layer.