Opening a Component Library to AI Agents: /api/registry
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 CategoryComments (0)
Newsletter
Stay updated! Get all the latest and greatest posts delivered straight to your inbox