05Project
Astro example
A static Astro site that renders a task list and one page per task at build time, through a client generated from an OpenAPI 3.1 document (openapi.yaml). The swaggerz/astro integration generates the client: there is no separate generate step.
pnpm example:astro # from the repository root: build swaggerz, start astro devOr, inside this directory once swaggerz is built:
pnpm dev # http://localhost:4321: generates src/generated/api, regenerates when openapi.yaml changespnpm check # builds the site (generating the client first), then type-checks it (astro check)pnpm dashboard # explore the API and the generated codeWhat to look at:
- astro.config.ts adds
swaggerz()fromswaggerz/astro.astro devgenerates the client when it starts and again whenever openapi.yaml or swaggerz.config.ts changes (errors show over the page, as Vite’s overlay);astro buildandastro checkgenerate it first. See framework integrations. - src/pages/index.astro lists the tasks with
listTasks; src/pages/tasks/[id].astro builds one page per task withgetStaticPathsandgetTask, and handles a404with the operation’s error guard, which types the body by status. - src/api.ts configures the client once. Its requests are answered in process by the generated mock handlers through
mockFetch()(src/mocks.ts): no server and no network, at build time and inastro dev. To call the real API, drop the transport and setbaseUrl. See mocks.
Config
import { defineConfig } from 'swaggerz';
export default defineConfig({ spec: { version: '3.1', input: './openapi.yaml' }, output: { directory: './src/generated/api' }, // Mock Service Worker handlers and fixtures: src/api.ts answers the client's requests with them. features: { mocks: 'msw' },});Sources
import { defineConfig } from 'astro/config';import swaggerz from 'swaggerz/astro';
export default defineConfig({ // Generates src/generated/api from openapi.yaml: when `astro dev` starts and again whenever the // specification or swaggerz.config.ts changes (errors show over the page), and before // `astro build` and `astro check`. integrations: [swaggerz()],});/** * The generated client, configured once for the pages. Its requests are answered in process by * the generated mocks (src/mocks.ts): no server, no network. To call the real API, drop the * transport and set `baseUrl` to the API's URL. */import { client, createFetchTransport } from './generated/api/index.ts';import { mockFetch } from './generated/api/mocks/index.ts';import { handlers } from './mocks.ts';
client.setConfig({ baseUrl: 'https://tasks.example.com/api', transport: createFetchTransport({ fetch: mockFetch(handlers) }),});
export { getTask, isGetTaskApiError, listTasks } from './generated/api/index.ts';---interface Props { title: string;}
const { title } = Astro.props;---
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>{title}</title> </head> <body> <main> <slot /> </main> </body></html>
<style is:global> body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.5; color: #1f2328; background: #fbfaf7; } main { max-width: 36rem; margin: 0 auto; padding: 2rem 1rem; } a { color: #0b5cad; } .status { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6e7781; }</style>/** * The Tasks API, answered in this process by the generated Mock Service Worker handlers over a * fixed list of tasks. Typed with the generated models: it stops compiling when the * specification changes. */import { HttpResponse } from 'msw';import type { Problem, Task } from './generated/api/index.ts';import { makeTask, mockGetTask, mockListTasks } from './generated/api/mocks/index.ts';
const tasks: Task[] = [ makeTask({ id: '3', title: 'Ship the release', status: 'todo', notes: null }), makeTask({ id: '2', title: 'Write the changelog', status: 'doing', notes: 'Group by feature' }), makeTask({ id: '1', title: 'Generate the client', status: 'done', notes: null }),];
export const handlers = [ mockListTasks({ body: ({ query }) => { const status = query.get('status'); return { items: tasks.filter((task) => status === null || task.status === status) }; }, }), mockGetTask({ body: ({ params }) => tasks.find((task) => task.id === params.taskId) ?? HttpResponse.json({ title: 'Not found' } satisfies Problem, { status: 404 }), }),];---import Page from '../layouts/Page.astro';import { listTasks } from '../api.ts';
// Runs at build time (and on each request during `astro dev`), through the generated client.const { items } = await listTasks({ limit: 50 });---
<Page title="Tasks"> <h1>Tasks</h1> <ul> { items.map((task) => ( <li> <a href={`/tasks/${task.id}/`}>{task.title}</a> <span class="status">{task.status}</span> </li> )) } </ul></Page>---import type { GetStaticPaths } from 'astro';import Page from '../../layouts/Page.astro';import { getTask, isGetTaskApiError, listTasks } from '../../api.ts';
// One page per task, listed through the generated client at build time.export const getStaticPaths = (async () => { const { items } = await listTasks({ limit: 50 }); return items.map((task) => ({ params: { id: task.id } }));}) satisfies GetStaticPaths;
const { id } = Astro.params;let task;try { task = await getTask({ taskId: id });} catch (error) { // Typed by status: a 404 from getTask has a Problem body. if (isGetTaskApiError(error) && error.status === 404) return Astro.redirect('/'); throw error;}---
<Page title={task.title}> <p><a href="/">All tasks</a></p> <h1>{task.title}</h1> <p class="status">{task.status}</p> {task.notes !== null && task.notes !== undefined && <p>{task.notes}</p>} <p>Created {new Date(task.createdAt).toLocaleDateString('en', { dateStyle: 'long' })}</p></Page>Generated files
36 files for 5 operations and 6 models, generated with this config when the site was built:
client.ts, index.tsmocks/16 filesmodels/6 filesruntime/7 filestasks/5 files