05Project
Next.js example
A Next.js App Router site whose server components render a task list and one page per task, through a client generated from an OpenAPI 3.1 document (openapi.yaml). withSwaggerz() from swaggerz/next generates the client: there is no separate generate step.
pnpm example:next # from the repository root: build swaggerz, start next devOr, inside this directory once swaggerz is built:
pnpm dev # http://localhost:3000: generates src/generated/api, regenerates when openapi.yaml changespnpm check # builds the site (generating the client first, then type-checking it)pnpm dashboard # explore the API and the generated codeWhat to look at:
- next.config.ts wraps the config with
withSwaggerz().next devgenerates the client when it starts and again whenever openapi.yaml or swaggerz.config.ts changes (problems are printed in the terminal);next buildgenerates it first and fails when it cannot. See framework integrations. - swaggerz.config.ts sets
output.importExtension: 'none': neither Turbopack nor webpack in Next.js resolves the default./client.jsimports toclient.tsfiles.swaggerz initsets it in projects that depend onnext. - src/app/page.tsx lists the tasks with
listTasks; src/app/tasks/[id]/page.tsx prerenders one page per task withgenerateStaticParamsandgetTask, and turns a404intonotFound()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 innext 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' }, // Next.js (Turbopack and webpack) does not resolve `./client.js` to `client.ts`: generate // imports without an extension. output: { directory: './src/generated/api', importExtension: 'none' }, // Mock Service Worker handlers and fixtures: src/api.ts answers the client's requests with them. features: { mocks: 'msw' },});Sources
import type { NextConfig } from 'next';import { withSwaggerz } from 'swaggerz/next';
const nextConfig: NextConfig = { reactStrictMode: true,};
// Generates src/generated/api from openapi.yaml: when `next dev` starts and again whenever the// specification or swaggerz.config.ts changes, and before `next build`.export default withSwaggerz(nextConfig);/** * The generated client, configured once for the server components. 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';import { mockFetch } from './generated/api/mocks';import { handlers } from './mocks';
client.setConfig({ baseUrl: 'https://tasks.example.com/api', transport: createFetchTransport({ fetch: mockFetch(handlers) }),});
export { getTask, isGetTaskApiError, listTasks } from './generated/api';import type { Metadata } from 'next';import type { ReactNode } from 'react';import './styles.css';
export const metadata: Metadata = { title: 'Tasks' };
export default function RootLayout({ children }: { readonly children: ReactNode }) { return ( <html lang="en"> <body> <main>{children}</main> </body> </html> );}import Link from 'next/link';import { listTasks } from '../api';
// A server component: rendered at build time, through the generated client.export default async function Tasks() { const { items } = await listTasks({ limit: 50 }); return ( <> <h1>Tasks</h1> <ul> {items.map((task) => ( <li key={task.id}> <Link href={`/tasks/${task.id}`}>{task.title}</Link>{' '} <span className="status">{task.status}</span> </li> ))} </ul> </> );}import Link from 'next/link';import { notFound } from 'next/navigation';import { getTask, isGetTaskApiError, listTasks } from '../../../api';
// One page per task, listed through the generated client at build time.export async function generateStaticParams() { const { items } = await listTasks({ limit: 50 }); return items.map((task) => ({ id: task.id }));}
async function load(id: string) { try { return await getTask({ taskId: id }); } catch (error) { // Typed by status: a 404 from getTask has a Problem body. if (isGetTaskApiError(error) && error.status === 404) notFound(); throw error; }}
export default async function Task({ params }: { readonly params: Promise<{ id: string }> }) { const task = await load((await params).id); return ( <> <p> <Link href="/">All tasks</Link> </p> <h1>{task.title}</h1> <p className="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> </> );}/** * 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';import { makeTask, mockGetTask, mockListTasks } from './generated/api/mocks';
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 }), }),];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