Files
suaveplan/openspec/archive/2026-05-26-api-graphql-ui-graphiql/proposal.md
2026-05-27 23:29:53 +01:00

4.1 KiB

@suaveplan/api-graphql-ui-graphiql — GraphiQL UI mount at /graphql/ui

Package: @suaveplan/api-graphql-ui-graphiql Status: PROPOSED Type: new-package Stage: 7 (GraphQL + WebSocket / SSE engines) — see umbrella openspec/changes/api-plugin-foundation/

Why

Per design doc §10.6 the foundation reserves GET /graphql/ui for an interactive query explorer but does not bundle one by default — UI choice is opt-in via a separate package so the host's runtime image stays minimal for production deployments that don't need it. GraphiQL is the default option called out in §10.6 ("GET /graphql/ui — GraphiQL (or Altair via api-graphql-ui-altair)"). Operators wire this package when they want the canonical GraphQL UI; they wire api-graphql-ui-altair instead when they prefer Altair.

The GraphQL engine adapters (api-graphql-yoga, api-graphql-apollo) intentionally return 404 at /graphql/ui when no UI plugin is wired — so this package is the standard way to give developers an in-browser explorer.

What changes

Net-new package depending on @suaveplan/api-types, @suaveplan/api-router, @suaveplan/api-graphql, @suaveplan/error, @suaveplan/logger, and graphiql (peer; static assets served from this package, not fetched from a CDN).

  • ADDED: createGraphiqlUi(opts) — factory returning a RouterPlugin value that registers a GET <uiEndpoint> handler serving the GraphiQL HTML shell + assets.
  • ADDED: GraphiqlUiOptions — Zod-validated config: uiEndpoint (default /graphql/ui), graphqlEndpoint (default /graphql), subscriptionEndpoint (default /graphql/ws), theme ("light" | "dark" | "system", default "system"), defaultQuery, defaultVariableEditorOpen, editorOptions (codemirror passthroughs).
  • ADDED: Static-asset serving — GraphiQL's bundle is served from the package's own dist/assets/ (no CDN dependency). The HTML shell embeds versioned hashes for cache-busting.
  • ADDED: CSP/security headers — Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline' (GraphiQL needs inline styles) emitted on the HTML response.
  • ADDED: GRAPHQL_UI_GRAPHIQL_* error codes: GRAPHQL_UI_GRAPHIQL_ASSET_MISSING, GRAPHQL_UI_GRAPHIQL_ENDPOINT_COLLISION.
  • ADDED: OTel instruments: graphql.ui.requests_total { ui: "graphiql" } counter.

Out of scope

  • A CDN-fetched GraphiQL variant — bundled assets only, per the foundation's "no runtime CDN fetches" rule (security + offline-dev posture).
  • The Altair alternative — separate package api-graphql-ui-altair.
  • Schema introspection — that's the engine adapter's job (Yoga / Apollo configure introspection on /graphql). This package just renders an explorer that calls into whatever the engine exposes.
  • Authentication on the UI route — host config decides whether /graphql/ui is behind auth; this package surfaces a RouterPlugin whose route options can be configured by the operator.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
GET /graphql/ui default to GraphiQL §10.6
UI as opt-in via separate package §10.6 + cross-ref to engine adapters returning 404 when not wired
Theme / config passthroughs §10.6 (implied — interactive explorer)

Verification approach

  • src/factory/create-graphiql-ui.test.ts — factory returns a valid RouterPlugin; HTML response shape; CSP header presence; asset 200s.
  • src/factory/create-graphiql-ui.spec-conformance.test.ts — pins that the mounted endpoint serves at uiEndpoint and that the response embeds the configured graphqlEndpoint + subscriptionEndpoint.
  • src/assets/assets.test.ts — every bundled asset reachable; missing-asset path raises GRAPHQL_UI_GRAPHIQL_ASSET_MISSING.
  • tests/integration/graphiql-end-to-end.test.ts (30000ms) — boots api-graphql-yoga + this UI plugin, opens a Playwright browser at /graphql/ui, runs a query through the live editor, asserts results render.

Migration impact

None — additive only.