05Project
SWR example
The task list of the React Query example, built with SWR hooks and ArkType validation, from the same OpenAPI 3.1 document (openapi.yaml).
pnpm example:swr # from the repository root: build swaggerz, generate, start ViteOr, inside this directory once swaggerz is built:
pnpm generate # src/generated/apipnpm dev # http://localhost:5173, served by an in-memory mock API (mock-api.ts)pnpm check # type-checks the app and builds itWhat to look at:
- swaggerz.config.ts enables SWR hooks (
features.swr), an infinite list forlistTaskspaginated by itscursorparameter, revalidation of everyTasksquery after mutations, and ArkType validation of responses. - src/App.tsx uses
useListTasksInfinite(pages andsetSize),useGetTask, anduseCreateTask/useUpdateTask/useDeleteTaskwithtrigger. Errors are typed:ApiErrorwith the operation’s error body (Problem) orRequestError(network, timeout, or a response that failed validation). - mock-api.ts implements the API with the generated model types, so it stops compiling when the specification changes.
Config
import { defineConfig } from 'swaggerz';
export default defineConfig({ spec: { version: '3.1', input: './openapi.yaml' }, output: { directory: './src/generated/api' }, client: { baseUrl: '/api' }, features: { swr: { // Load more tasks with `useListTasksInfinite`, driven by the `cursor` query parameter. infinite: { listTasks: { pageParam: 'cursor' } }, // Revalidate every cached task query after a change. invalidates: { createTask: ['Tasks'], updateTask: ['Tasks'], deleteTask: ['Tasks'] }, }, // Check responses at runtime against the specification with ArkType. validation: 'arktype', },});Sources
import { useState, type FormEvent } from 'react';import { isApiError, type OperationError, type Task, type TaskStatus } from './generated/api';import { useCreateTask, useDeleteTask, useGetTask, useListTasksInfinite, useUpdateTask,} from './generated/api/swr';
const STATUSES: readonly TaskStatus[] = ['todo', 'doing', 'done'];
/** A readable message for any error an operation can reject with. */function errorMessage(error: OperationError<unknown>): string { if (isApiError(error)) { const body = error.body as { title?: unknown } | undefined; return typeof body?.title === 'string' ? body.title : `HTTP ${error.status}`; } // RequestError: network failure, timeout, or a response that failed ArkType validation. return `${error.kind}: ${error.message}`;}
export function App() { const [status, setStatus] = useState<TaskStatus | undefined>(undefined); const [selected, setSelected] = useState<string | undefined>(undefined);
// Every parameter is part of the key, so each filter is cached separately. const tasks = useListTasksInfinite( { status, limit: 2 }, { initialPageParam: undefined, getNextPageParam: (page) => page.nextCursor }, ); const pages = tasks.data ?? []; const hasMore = pages.at(-1)?.nextCursor !== undefined;
return ( <main style={{ fontFamily: 'system-ui', maxWidth: 640, margin: '2rem auto', padding: '0 1rem' }} > <h1>Tasks</h1> <NewTaskForm /> <p> Show:{' '} <select name="status" value={status ?? ''} onChange={(event) => { const value = STATUSES.find((s) => s === event.target.value); setStatus(value); }} > <option value="">all</option> {STATUSES.map((s) => ( <option key={s} value={s}> {s} </option> ))} </select> </p> {tasks.isLoading && <p>Loading…</p>} {tasks.error !== undefined && <p role="alert">{errorMessage(tasks.error)}</p>} <ul> {pages .flatMap((page) => page.items) .map((task) => ( <TaskItem key={task.id} task={task} onSelect={() => setSelected(task.id)} /> ))} </ul> {hasMore && ( <button disabled={tasks.isValidating} onClick={() => void tasks.setSize(tasks.size + 1)}> {tasks.isValidating ? 'Loading…' : 'Load more'} </button> )} {selected !== undefined && <TaskDetails taskId={selected} />} </main> );}
function NewTaskForm() { const [title, setTitle] = useState(''); // `invalidates` in swaggerz.config.ts revalidates the task lists after the mutation succeeds. const createTask = useCreateTask();
async function submit(event: FormEvent) { event.preventDefault(); await createTask.trigger({ body: { title } }); setTitle(''); }
return ( <form onSubmit={(event) => void submit(event).catch(() => undefined)}> <input name="title" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="New task" /> <button disabled={createTask.isMutating}>Add</button> {createTask.error !== undefined && <p role="alert">{errorMessage(createTask.error)}</p>} </form> );}
function TaskItem({ task, onSelect }: { task: Task; onSelect: () => void }) { const updateTask = useUpdateTask(); const deleteTask = useDeleteTask(); const next: TaskStatus = task.status === 'todo' ? 'doing' : 'done';
return ( <li> <button onClick={onSelect}>{task.title}</button> <small>{task.status}</small>{' '} {task.status !== 'done' && ( <button onClick={() => void updateTask.trigger({ taskId: task.id, body: { status: next } })} > → {next} </button> )} <button onClick={() => void deleteTask.trigger({ taskId: task.id })}>Delete</button> </li> );}
function TaskDetails({ taskId }: { taskId: string }) { const task = useGetTask({ taskId });
if (task.error !== undefined) return <p role="alert">{errorMessage(task.error)}</p>; return ( <aside> <h2>Details</h2> <p> Created: {task.data === undefined ? '…' : new Date(task.data.createdAt).toLocaleString()} </p> <p>{task.data?.notes ?? 'No notes'}</p> </aside> );}import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import { SWRConfig } from 'swr';import { App } from './App.tsx';
const root = document.getElementById('root');if (root === null) throw new Error('Missing #root element.');createRoot(root).render( <StrictMode> <SWRConfig value={{ dedupingInterval: 10_000, shouldRetryOnError: false }}> <App /> </SWRConfig> </StrictMode>,);Generated files
35 files for 5 operations and 6 models, generated with this config when the site was built:
arktype/8 filesclient.ts, index.tsmodels/6 filesruntime/7 filesswr/7 filestasks/5 files