Skip to content

Opening a Component Library to AI Agents: /api/registry

7/28/2025Data Science & AIKUIreact6 min read

Opening a Component Library to AI Agents: /api/registry

An API endpoint that returns the component library's metadata — for humans, for AI agents, and for documentation tools. One endpoint, three consumers.

A component library grows and people stop knowing what's in it. Developers search for a component they half-remember, or build something that already exists. AI coding assistants hallucinate component names because they don't have access to what's actually available.

kui-react solves this with a /api/registry endpoint that exposes the entire component catalog as structured JSON. The design is simple, but the implications are broader than they appear.

The Registry Type

// modules/registry/registry.types.ts
interface RegistryComponent {
  id: string;
  label: string;
  description: string;
  source: string;          // full source code
  variants: RegistryVariant[];
  whenToUse: string[];
  whenNotToUse: string[];
  composes: string[];      // component IDs this one is built from
  relatedTo: string[];     // conceptually related components
  usedBy: string[];        // which higher-level components use this
  a11y: {
    roles: string[];
    ariaAttributes: string[];
    keyboardInteractions: string[];
  };
  designTokens: string[];  // CSS variables this component references
  dependencies: string[];  // npm packages required
  external: boolean;       // third-party component wrapper?
}

interface Registry {
  layers: Record<string, string>;     // layer descriptions
  conventions: Record<string, string>; // naming/structure rules
  designTokens: Record<string, string>; // token definitions
  components: RegistryComponent[];
  themes: string[];
}

a11y, whenToUse, whenNotToUse — these aren't for rendering. They're for the consumers who need to make decisions: "should I use this component here?" or "what accessibility concerns does this bring?"

The Endpoint

// app/api/registry/route.ts
export const dynamic = 'force-dynamic';

let cached: Registry | null = null;

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const indexMode = searchParams.get('index') === '1';

  if (!cached) {
    const filePath = path.join(process.cwd(), 'public/registry/components.json');
    const raw = await fs.readFile(filePath, 'utf-8');
    cached = JSON.parse(raw) as Registry;
  }

  let data: unknown = cached;

  if (indexMode) {
    // Strip source code, collapse variants to count
    data = {
      ...cached,
      components: cached.components.map(({ source, variants, ...rest }) => ({
        ...rest,
        variantCount: variants.length,
      })),
    };
  }

  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, max-age=3600, s-maxage=3600',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

export const dynamic = 'force-dynamic' — Next.js App Router would otherwise try to statically generate this route. The registry file is read from public/registry/components.json, a static snapshot generated at build time.

The module-level cached variable avoids reading the file on every request. It's populated on first request and reused. Cache-Control headers push caching to CDN and browser — the endpoint can handle high traffic without hitting the filesystem repeatedly.

?index=1 mode strips source (which can be several KB per component) and reduces variants to a count. This produces a lightweight catalog for consumers that only need to know what exists, not the full implementation.

Three Consumers

Documentation site. The full registry (without ?index=1) feeds the interactive component browser. Each component page shows its source, variants, accessibility notes, and design token dependencies — all from the same JSON.

AI coding assistants. When a developer asks an AI to "add a date picker to this form," the AI can call GET /api/registry?index=1 first to discover what's available. The whenToUse, whenNotToUse, and composes fields give it enough context to recommend the right component and use it correctly.

llms-full.txt. The endpoint also generates an LLM-friendly plain text file at /public/registry/llms-full.txt — the entire registry as structured text, formatted for prompt injection. This covers AI assistants that can't make HTTP calls but can read static files.

The CONVENTIONS Object

// modules/registry/registry.ts
const CONVENTIONS = {
  icons: 'All icon props accept React.ReactNode. Pass any icon library.',
  styling: 'className prop extends, never replaces, default styles.',
  types: 'Full TypeScript. Generic props where polymorphism is needed.',
  accessibility: 'WCAG 2.1 AA. aria-* and role attributes included.',
  fileNaming: 'PascalCase components, camelCase hooks, kebab-case files.',
  pathAlias: '@/ maps to the project root.',
};

The conventions object is included in the registry response. An AI agent reading the registry immediately knows the icon system, how className works, and the accessibility baseline — without reading component source code.

LAYER_DESCRIPTIONS

const LAYER_DESCRIPTIONS = {
  ui: 'Primitive UI components. No business logic.',
  app: 'Composed application features. May contain domain concepts.',
  domain: 'Business logic and data models. No UI.',
  theme: 'Design tokens and global styles.',
  library: 'Third-party library wrappers and adapters.',
};

These descriptions explain the architectural layers to any consumer that needs to understand where to look for a component or where to add a new one.

Static Snapshot vs. Live Generation

The registry is pre-generated at build time into public/registry/components.json rather than computed dynamically from source files. This is a deliberate trade-off.

Dynamic generation would always be up-to-date but adds build-time complexity: parsing TypeScript source, extracting JSDoc, resolving imports. Static generation runs once during npm run build, produces a deterministic output, and the result is just a JSON file.

The cost: the registry can be stale if someone forgets to regenerate after adding a component. The build pipeline should include registry generation as a step — if it's missing from CI, the registry drifts.

Trade-off

Access-Control-Allow-Origin: * makes the registry public. Anyone can read the component catalog from any origin. For a public component library this is intentional. For a closed-source internal library, this CORS header should be restricted to known origins.

The module-level cached variable is not request-isolated — all concurrent requests share it. In a multi-tenant scenario this is fine (the registry is the same for all users). In a scenario where the registry could be user-specific, this approach breaks.

force-dynamic disables static generation for the route. In a deployment where the registry never changes between deployments, force-static with a build-time regeneration step would be more efficient.

Business Impact

When AI-assisted development is in the workflow — GitHub Copilot, Claude, Cursor — the quality of suggestions depends on what the model knows about the codebase. A component library without discoverable metadata gets hallucinated usage. One with a structured registry endpoint gets accurate, contextual suggestions.

The investment is one endpoint and a build-time generation step. The return is every developer (and AI assistant) knowing what's available and how to use it correctly.

Something to Try

If your component library doesn't have a machine-readable registry, create a minimal one: a JSON file with component name, description, props, and whenToUse. Serve it as a static file or a simple API endpoint. Then paste it into a Claude or GPT conversation context before asking "how should I build this form?" The quality difference in suggestions is immediate.

Related Articles

Same Category

Comments (0)

Newsletter

Stay updated! Get all the latest and greatest posts delivered straight to your inbox