# Svelte Lean Query

> Server state for Svelte 5: endpoint definitions, cache, invalidation by tags, loading states and writes on runes; a live example, the install command, the levels, and the entry points with their stability labels and measured sizes.

Page: https://svelte-lean.vercel.app/query · Markdown: https://svelte-lean.vercel.app/query.md · Index: https://svelte-lean.vercel.app/llms.txt

Server state for Svelte 5: endpoints defined once, a cache keyed by params, invalidation by typed tags, and the loading states a screen needs, built on runes.

## At a glance

Search, then pin a member. The panel lists what the server received and why: `mount`, `params`, `mutation`, `invalidate`.

```svelte
<!-- src/routes/members/+page.svelte -->
<script lang="ts">
	import { createMutation, createQuery } from '@svelte-lean/query';
	import { listMembers, pinMember } from '$api/members';

	let filters = $state({ search: '' });
	const members = createQuery(listMembers, () => filters, {
		keepPrevious: true,
		poll: () => '30s'
	});
	const pin = createMutation(pinMember);
</script>

<input type="search" aria-label="Search members" bind:value={filters.search} />
{#if members.loading}<p>Loading members…</p>{/if}
{#if members.refreshing}<progress aria-label="Refreshing"></progress>{/if}

<ul>
	{#each members.data?.rows ?? [] as member (member.id)}
		<li>
			{member.name}
			<button
				disabled={pin.busy || !pin.allowed()}
				onclick={() => pin.run({ id: member.id, pinned: !member.pinned })}
			>
				{member.pinned ? 'Unpin' : 'Pin'}
			</button>
		</li>
	{/each}
</ul>
```

The component above is the file shown; an in-memory server answers in place of http(), with a fixed delay.

`src/lib/api/members.ts`

```ts
import { defineMutation, defineQuery, page, tag, type } from '@svelte-lean/query';
import type { Member, MemberFilters, MemberInput, Team } from './types';

// The tag and permission registries: a typo in a tag or a permission is a compile error.
declare module '@svelte-lean/query' {
	interface Tags {
		Member: number;
		Members: void;
		Teams: void;
	}
	interface Permissions {
		'members.view': true;
		'members.edit': true;
		'members.delete': true;
	}
}

export const listMembers = defineQuery({
	get: 'api/members',
	params: type<MemberFilters>(),
	result: page<Member>(),
	entity: 'Member', // every row carries tag('Member', row.id)
	tags: ['Members'],
	fresh: '15s',
	access: 'members.view'
});

export const pinMember = defineMutation({
	put: 'api/members/{id}/pin',
	body: type<{ pinned: boolean }>(),
	result: type<Member>(),
	invalidates: (member) => [tag('Member', member.id)],
	queue: (input) => `member:${input.id}` // one write per member at a time, in order
});
```

`src/routes/+layout.svelte`

```svelte
<!-- src/routes/+layout.svelte — one client for the application -->
<script lang="ts">
	import { createClient, setClient } from '@svelte-lean/query';
	import { http } from '@svelte-lean/query/http';
	import { can, session } from '$api/session.svelte';

	let { children } = $props();

	setClient(
		createClient({
			transport: http({
				base: '/',
				headers: () => ({ authorization: `Bearer ${session.token}` })
			}),
			scope: () => [session.workspace], // part of every cache key
			can // resolves the permission strings of `access` rules
		})
	);
</script>

{@render children()}
```

### What happens without more code

- Typing changes the params; the previous rows stay on screen while the next ones load.
- A request whose answer nobody waits for any more is aborted.
- The list polls only while the tab is visible; a poll sets `refreshing`, never `loading`.
- Pinning refetches only the observed entries tagged `Member:<id>`, once, even when a poll lands at the same time.
- Without the `members.view` permission, nothing is sent.

## Install

One package; every entry point below is a subpath of it. Its only peer dependency is `svelte ^5.20.0`. Then create the client: [Client setup](https://svelte-lean.vercel.app/query/client.md).

```bash
npm install @svelte-lean/query
```

## Levels

A screen should need level 1 only. The guides and the reference follow the same order.

| Level | Names | Used for | Guides |
| --- | --- | --- | --- |
| 1 · every day | `defineQuery``defineMutation``createQuery``createMutation``run``data``loading``refreshing``error``busy` | Most screens | [Defining endpoints](https://svelte-lean.vercel.app/query/endpoints.md)[Reading](https://svelte-lean.vercel.app/query/reading.md)[Writing](https://svelte-lean.vercel.app/query/writing.md) |
| 2 · often | `keepPrevious``poll``when``load()``invalidate()``patch()``optimistic``createPaged``createInfinite``allowed()` | Tables, dialogs, forms | [Reading](https://svelte-lean.vercel.app/query/reading.md#when-a-query-runs)[Lists](https://svelte-lean.vercel.app/query/lists.md)[Forms](https://svelte-lean.vercel.app/query/forms.md) |
| 3 · advanced | `info``{ loading }``reason``entity``defineModule``send()``parts``live``persist` | Complex pages, realtime | [Realtime](https://svelte-lean.vercel.app/query/realtime.md)[Persistence](https://svelte-lean.vercel.app/query/persistence.md)[Server rendering](https://svelte-lean.vercel.app/query/ssr.md) |

## When a query runs

A query starts when its result is read on screen. Everything else narrows that.

| Need | How | Recipe |
| --- | --- | --- |
| It should load when shown | `nothing: read it inside {#if}` | [Load when a dialog opens](https://svelte-lean.vercel.app/query/recipes/dialog.md) |
| A state condition: dialog, tab | `when: () => open` | [Reading](https://svelte-lean.vercel.app/query/reading.md#when) |
| A required input is missing | `params return undefined` | [Dependent queries](https://svelte-lean.vercel.app/query/recipes/dependent-queries.md) |
| A button starts it | `{ when: false } + load(params)` | [Search button](https://svelte-lean.vercel.app/query/recipes/search-button.md) |

## Entry points

Each module is its own import; the root entry never re-exports one. A screen that reads and writes ships 15409 B gzip; a screen that only reads ships 11787 B gzip.

| Entry point | Stability | Contains | Measured | gzip | brotli |
| --- | --- | --- | --- | --- | --- |
| [`@svelte-lean/query`](https://svelte-lean.vercel.app/query/api.md) | stable target | Definitions, the client, createQuery, createMutation, invalidate, patch, errors | queryquery+mutationroot (every export) | 11787 B15409 B16134 B | 10839 B14167 B14844 B |
| [`./http`](https://svelte-lean.vercel.app/query/api/http.md) | stable target | The fetch transport: base, headers, timeout, typed errors, uploads, downloads | http (marginal) | 2239 B | 2079 B |
| [`./laravel`](https://svelte-lean.vercel.app/query/api/laravel.md) | stable target | A platform profile: envelope, refusals, field errors, paginators, query strings | laravel (marginal) | 1681 B | 1548 B |
| [`./paged`](https://svelte-lean.vercel.app/query/api/paged.md) | stable target | Server-paged lists and the props of \<DataTable manual\> | paged (marginal) | 1234 B | 1109 B |
| [`./forms`](https://svelte-lean.vercel.app/query/api/forms.md) | stable target | Server field errors into native form state | forms (marginal) | 1406 B | 1226 B |
| [`./parts`](https://svelte-lean.vercel.app/query/api/parts.md) | stable target | Optional engine parts included up front with createClient({ parts }) | every part (static, marginal)query + every part (static) | 3880 B15667 B | 3544 B14383 B |
| [`./testing`](https://svelte-lean.vercel.app/query/api/testing.md) | stable target | A test client with virtual time and typed mocks; MSW handlers | testing (not shipped) | 18878 B | 17235 B |
| [`./infinite`](https://svelte-lean.vercel.app/query/api/infinite.md) | experimental | "Load more" over ordinary page entries | infinite (marginal) | 1648 B | 1502 B |
| [`./live`](https://svelte-lean.vercel.app/query/api/live.md) | experimental | Socket events that invalidate or patch cached entries | live (marginal) | 2777 B | 2565 B |
| [`./tabs`](https://svelte-lean.vercel.app/query/api/tabs.md) | experimental | One poller and one socket per browser, shared by every tab | tabs (marginal) | 3104 B | 2874 B |
| [`./persist`](https://svelte-lean.vercel.app/query/api/persist.md) | experimental | Opt-in IndexedDB persistence per endpoint and per session | persist (marginal) | 3428 B | 3201 B |
| [`./ssr`](https://svelte-lean.vercel.app/query/api/ssr.md) | experimental | SvelteKit server rendering, dehydrate and hydrate, load invalidation | ssr (marginal)ssr every export (marginal) | 568 B1287 B | 514 B1178 B |
| [`./devtools`](https://svelte-lean.vercel.app/query/api/devtools.md) | experimental | Development detectors, a timeline and an in-page panel; a stub in production | devtools (production stub) | 92 B | 68 B |
| [`query-codegen`](https://svelte-lean.vercel.app/query/api/codegen.md) | experimental | A development tool: OpenAPI 3.x document to endpoint modules | a development tool; nothing ships to the browser |  |  |

Consumer entries bundled with Vite in production mode (Rollup tree-shaking, esbuild minify, ESM, es2022, production condition, svelte external, .svelte.js compiled with compileModule). "query" = defineQuery + createQuery + client (first-load code; optional parts are their own "part:" rows, loaded on first use); "query+mutation" adds defineMutation, createMutation, patch; "query + every part (static)" adds \`allParts\` from \`./parts\` (every optional part in the first load, no lazy chunk). Vite's preload helper is the application's and not counted. Marginal rows subtract the row named in "over". gzip level 9 / brotli quality 11 (node:zlib). Budgets are gzip; "target" is the 04 §2 target. Budgets, targets and every other measurement: [Performance](https://svelte-lean.vercel.app/query/performance.md).

## Where to go next

- [Client setup](https://svelte-lean.vercel.app/query/client.md), then [Defining endpoints](https://svelte-lean.vercel.app/query/endpoints.md).
- Coming from another library: [TanStack Query](https://svelte-lean.vercel.app/query/migration/tanstack-query.md) or [RTK Query](https://svelte-lean.vercel.app/query/migration/rtk-query.md).
- Task by task: [Recipes](https://svelte-lean.vercel.app/query/recipes/dialog.md).
