Skip to content

Batteries-included served GraphQL search API + self-contained playground #600

Description

@ddeboer

Summary

Make @lde/search-api-graphql batteries-included: today it ships only buildGraphQLSchema() (a GraphQLSchema), so every consumer hand-rolls the served endpoint. In the Dataset Register that meant a bespoke +server.ts (a hand-written graphql() call, Accept-Language parsing, error shaping) and no playgroundGET /graphql is a blank page.

Ship the served API and a self-contained playground, while keeping the schema composable so consumers can add their own queries/fields.

Complements #495: that issue generates the schema (from SHACL + search:); this one serves it. #495’s core – a working API with inline locale-resolved facet labels – is already delivered in @lde/search-api-graphql@0.5.0 and live in the Dataset Register.

Design: three layers, each usable and replaceable

  1. buildGraphQLSchema(searchSchema) → GraphQLSchema (exists). Full control; extend freely in code (@graphql-tools/schema mergeSchemas).
  2. A framework-agnostic Web-fetch handlercreateSearchGraphQLHandler({ searchSchema | schema, engine }) → (Request) => Promise<Response> handling POST execution, the GET playground, introspection, error shaping, and Accept-Language → language order. Because SvelteKit, Fastify, Hono and Node all speak Request/Response, it mounts anywhere in ~3 lines. It takes either a searchSchema (default) or a supplied, extended GraphQLSchema (bring-your-own fields).
  3. A prebuilt Docker image – the handler plus a boot entrypoint that loads mounted definitions (SHACL + search:) and engine config (Typesense host/key, collections, locales via env), then serves /graphql + playground. A thin wrapper over layer 2, not a separate codebase. For turnkey / non-JS / ops-driven deploys.

Base the server on graphql-yoga rather than hand-rolling: fetch-native, self-contained GraphiQL built in, introspection/errors/plugins, serves any executable schema. Framework-agnostic – no Fastify required.

#495 names Mercurius, which is Fastify-coupled. Since LDE serves many hosts (the Dataset Register runs on SvelteKit adapter-node), I propose Yoga instead – flagged as a deliberate deviation from the draft stack doc.

Extensibility

A consumer merges their own GraphQLSchema with buildGraphQLSchema()’s output and passes the union to the handler; Yoga serves their custom queries and the generated search API under one endpoint and one playground – no fork. Layers 1–2 support custom code fields; the image (layer 3) serves the generated/declarative surface only – custom-code consumers use layer 2, or build FROM the image.

Playground

GraphiQL 5 (Explorer + History plugins) as the shipped default – it is what Yoga bundles self-contained, its Monaco editor + Explorer builder are a solid DX, and its Docs panel renders markdown (so SHACL descriptions surface once they arrive). The renderer is swappable (e.g. Altair) and disable-able per environment.

Rejected: Apollo Sandbox – best DX but the embed loads from Apollo’s CDN and ships schema/queries to Apollo, so it is neither self-contained nor acceptable data-governance for a public service.

Embeddable. The register hosts the playground on a stable URL, so it doubles as an embed target: a Docusaurus docs site gets a live client simply by <iframe>-ing it – same-origin to /graphql (no CORS, no docs-bundle weight) and always in sync with the deployed schema. The hosted playground is thus the single source of truth for both the standalone and embedded experiences.

Verified: GraphiQL 5 + Explorer runs live against the Dataset Register prod /graphql – introspects, builds queries against the real types, returns results with locale-resolved labels.

Public access and limits

The API is public and read-only, so the handler should:

  • send CORS headers, so browser clients can call it cross-origin (Yoga’s CORS plugin);
  • enforce a query depth / cost limit, since a public live client invites arbitrary queries (a Yoga cost-limit plugin).

Deployment: a dedicated search-API pod

The recommended topology for a consumer like the Dataset Register is to run the API as its own pod (the image), with the presentation app as a consumer of it:

  • Scaling / isolation – SSR is CPU-bound, the search API is I/O-bound; split, they scale and fail independently.
  • Ownership / security – the Typesense engine and its admin key leave the public-facing presentation pod for the API pod.
  • Dogfooding / platform fit – the consumer runs the same image LDE ships, matching GraphQL search API #495’s direction (GraphQL now, the REST twin later, from one projection).

Same-origin is preserved: /graphql is an ingress path, re-pointed to the API service – no subdomain, no CORS. Cost: one more pod, and presentation-side SSR search becomes a small in-cluster hop.

Sequencing: (1) keep the embedded handler already shipped in the Dataset Register; (2) build layers 2 + 3; (3) extract the Dataset Register’s GraphQL onto its own pod running the image and retire the embedded handler (downstream follow-up). Avoid a bespoke GraphQL pod before the image exists – that is throwaway wiring.

Out of scope

  • Field/type descriptions – sourced from SHACL sh:description via the generated schema (GraphQL search API #495). This issue only ensures descriptions, once present, surface in introspection and the printed SDL (graphql-js native).
  • Field/locale weighting (search:boost) – GraphQL search API #495.

Also

Expose printGraphQLSchema(schema) as an SDL endpoint so consumers can publish the contract and generate static docs (Magidoc / SpectaQL) in CI without a running server.

Acceptance criteria

  • A framework-agnostic fetch handler serving POST execution, GET playground, introspection, error shaping and Accept-Language, from a searchSchema or a supplied GraphQLSchema.
  • Self-contained playground (no external CDN), renderer configurable, disable-able per environment; GraphiQL 5 default.
  • Playground hosted on a stable, iframe-embeddable URL (a docs site can embed a live client via <iframe>).
  • CORS enabled for cross-origin browser clients, and a query depth/cost limit to cap abuse.
  • A consumer can merge custom fields into the served schema and reach them through the same endpoint and playground.
  • A prebuilt image serving /graphql + playground from mounted definitions and engine config.
  • SDL retrievable without a running introspection query.
  • Additive, non-breaking to existing buildGraphQLSchema consumers; versioned per semver.
  • The Dataset Register drops its hand-rolled +server.ts for the LDE handler (downstream).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions