05Project
Svelte Query example
The task list of the React Query example, built with Svelte 5 (runes) and TanStack Svelte Query 6, from the same OpenAPI 3.1 document (openapi.yaml).
pnpm example:svelte-query # 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 (svelte-check) and builds itWhat to look at:
- swaggerz.config.ts enables Svelte Query functions (
features.query.framework: 'svelte'), an infinite list forlistTaskspaginated by itscursorparameter and invalidation of everyTasksquery after mutations. - src/Tasks.svelte passes accessors to
createListTasksInfiniteQuery: reading the$statefilter inside them makes the query follow it. src/TaskDetails.svelte does the same with a prop and usesselect. Errors are typed:ApiErrorwith the operation’s error body (Problem) orRequestError. - 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: { query: { framework: 'svelte', // Load more tasks with `createListTasksInfiniteQuery`, driven by the `cursor` parameter. infinite: { listTasks: { pageParam: 'cursor' } }, // Refresh every task query after a change. invalidates: { createTask: ['Tasks'], updateTask: ['Tasks'], deleteTask: ['Tasks'] }, }, },});Sources
<script lang="ts"> import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; import Tasks from './Tasks.svelte';
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10_000 } } });</script>
<QueryClientProvider client={queryClient}> <Tasks /></QueryClientProvider><script lang="ts"> import { createCreateTaskMutation } from './generated/api/svelte-query'; import { errorMessage } from './errors.ts';
let title = $state(''); // `invalidates` in swaggerz.config.ts refreshes the task lists after the mutation succeeds. const createTask = createCreateTaskMutation();
function submit(event: SubmitEvent) { event.preventDefault(); createTask.mutate({ body: { title } }, { onSuccess: () => (title = '') }); }</script>
<form onsubmit={submit}> <input bind:value={title} name="title" placeholder="New task" /> <button disabled={createTask.isPending}>Add</button> {#if createTask.error}<p role="alert">{errorMessage(createTask.error)}</p>{/if}</form><script lang="ts"> import { createGetTaskQuery } from './generated/api/svelte-query'; import { errorMessage } from './errors.ts';
let { taskId }: { taskId: string } = $props();
// The accessor reads the prop, so the query follows it; `select` narrows the cached data and // its result type is inferred. const created = createGetTaskQuery( () => ({ taskId }), () => ({ select: (task) => new Date(task.createdAt).toLocaleString() }), ); const notes = createGetTaskQuery( () => ({ taskId }), () => ({ select: (task) => task.notes ?? 'No notes' }), );</script>
{#if created.error} <p role="alert">{errorMessage(created.error)}</p>{:else} <aside> <h2>Details</h2> <p>Created: {created.data ?? '…'}</p> <p>{notes.data}</p> </aside>{/if}<script lang="ts"> import type { Task, TaskStatus } from './generated/api'; import { createDeleteTaskMutation, createUpdateTaskMutation, } from './generated/api/svelte-query';
let { task, onselect }: { task: Task; onselect: () => void } = $props();
const updateTask = createUpdateTaskMutation(); const deleteTask = createDeleteTaskMutation(); const next: TaskStatus = $derived(task.status === 'todo' ? 'doing' : 'done');</script>
<li> <button onclick={onselect}>{task.title}</button> <small>{task.status}</small> {#if task.status !== 'done'} <button onclick={() => updateTask.mutate({ taskId: task.id, body: { status: next } })}> → {next} </button> {/if} <button onclick={() => deleteTask.mutate({ taskId: task.id })}>Delete</button></li><script lang="ts"> import type { TaskStatus } from './generated/api'; import { createListTasksInfiniteQuery } from './generated/api/svelte-query'; import { errorMessage } from './errors.ts'; import NewTaskForm from './NewTaskForm.svelte'; import TaskDetails from './TaskDetails.svelte'; import TaskItem from './TaskItem.svelte';
const STATUSES: readonly TaskStatus[] = ['todo', 'doing', 'done']; let status = $state<TaskStatus | undefined>(undefined); let selected = $state<string | undefined>(undefined);
// Parameters are accessors read reactively: changing the filter refetches, and every filter is // cached separately because every parameter is part of the query key. const tasks = createListTasksInfiniteQuery( () => ({ status, limit: 2 }), () => ({ initialPageParam: undefined, getNextPageParam: (page) => page.nextCursor }), );
function setStatus(event: Event) { const value = (event.currentTarget as HTMLSelectElement).value; status = STATUSES.find((s) => s === value); }</script>
<main style="font-family: system-ui; max-width: 640px; margin: 2rem auto; padding: 0 1rem"> <h1>Tasks</h1> <NewTaskForm /> <p> Show: <select name="status" value={status ?? ''} onchange={setStatus}> <option value="">all</option> {#each STATUSES as s (s)} <option value={s}>{s}</option> {/each} </select> </p> {#if tasks.isPending}<p>Loading…</p>{/if} {#if tasks.error}<p role="alert">{errorMessage(tasks.error)}</p>{/if} <ul> {#each tasks.data?.pages.flatMap((page) => page.items) ?? [] as task (task.id)} <TaskItem {task} onselect={() => (selected = task.id)} /> {/each} </ul> {#if tasks.hasNextPage} <button disabled={tasks.isFetchingNextPage} onclick={() => tasks.fetchNextPage()}> {tasks.isFetchingNextPage ? 'Loading…' : 'Load more'} </button> {/if} {#if selected !== undefined}<TaskDetails taskId={selected} />{/if}</main>import { isApiError, type OperationError } from './generated/api';
/** A readable message for any error an operation can reject with. */export 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 the client could not parse. return `${error.kind}: ${error.message}`;}import { mount } from 'svelte';import App from './App.svelte';
const target = document.getElementById('root');if (target === null) throw new Error('Missing #root element.');mount(App, { target });Generated files
27 files for 5 operations and 6 models, generated with this config when the site was built:
client.ts, index.tsmodels/6 filesruntime/7 filessvelte-query/7 filestasks/5 files