Skip to content

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).

Terminal window
pnpm example:swr # from the repository root: build swaggerz, generate, start Vite

Or, inside this directory once swaggerz is built:

Terminal window
pnpm generate # src/generated/api
pnpm dev # http://localhost:5173, served by an in-memory mock API (mock-api.ts)
pnpm check # type-checks the app and builds it

What to look at:

  • swaggerz.config.ts enables SWR hooks (features.swr), an infinite list for listTasks paginated by its cursor parameter, revalidation of every Tasks query after mutations, and ArkType validation of responses.
  • src/App.tsx uses useListTasksInfinite (pages and setSize), useGetTask, and useCreateTask/useUpdateTask/useDeleteTask with trigger. Errors are typed: ApiError with the operation’s error body (Problem) or RequestError (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

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

src/App.tsx
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>
);
}
src/main.tsx
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 files
  • client.ts, index.ts
  • models/6 files
  • runtime/7 files
  • swr/7 files
  • tasks/5 files

The project on GitHub