Skip to content
swaggerz

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.

Terminal window
pnpm example:astro # from the repository root: build swaggerz, start astro dev

Or, inside this directory once swaggerz is built:

Terminal window
pnpm dev # http://localhost:4321: generates src/generated/api, regenerates when openapi.yaml changes
pnpm check # builds the site (generating the client first), then type-checks it (astro check)
pnpm dashboard # explore the API and the generated code

What to look at:

  • astro.config.ts adds swaggerz() from swaggerz/astro. astro dev generates 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 build and astro check generate it first. See framework integrations.
  • src/pages/index.astro lists the tasks with listTasks; src/pages/tasks/[id].astro builds one page per task with getStaticPaths and getTask, and handles a 404 with 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 in astro dev. To call the real API, drop the transport and set baseUrl. See mocks.

Config

swaggerz.config.ts
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

astro.config.ts
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()],
});
src/api.ts
/**
* 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';
src/layouts/Page.astro
---
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>
src/mocks.ts
/**
* 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 }),
}),
];
src/pages/index.astro
---
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>
src/pages/tasks/[id].astro
---
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.ts
  • mocks/16 files
  • models/6 files
  • runtime/7 files
  • tasks/5 files

The project on GitHub