Query Tooling
Codegen
A development tool that writes endpoint modules from an OpenAPI document: the same defineQuery and defineMutation values you would write by hand, one module per tag, typed tags and permissions included.
Markdown version of this page, for assistants (Markdown for AI)
Note @svelte-lean/query-codegen is experimental: the API can still change in a minor release.
Stability per entry point: Entry points.
On this page
Install
A development dependency, with openapi-typescript for the types. Node >=22.18.0: the configuration file is TypeScript, loaded with
Node's own type stripping.
npm install --save-dev @svelte-lean/query-codegen openapi-typescriptpnpm add -D @svelte-lean/query-codegen openapi-typescriptyarn add -D @svelte-lean/query-codegen openapi-typescriptbun add -d @svelte-lean/query-codegen openapi-typescriptRun
# 1. Types for the document (openapi-typescript)
openapi-typescript api/openapi.json -o src/lib/api/schema.gen.ts
# 2. Endpoint modules
slq-codegen --spec api/openapi.json --types src/lib/api/schema.gen.ts --config slq.codegen.ts
# CI: exit 1 when a committed module differs from what the document produces
slq-codegen --checkEach run parses the document once and writes only the files whose content changed, so editors and Vite reload only what moved. Nothing of the tool ships to the browser; the generated modules tree-shake per endpoint like hand-written ones.
| Flag | Meaning |
|---|---|
--spec | OpenAPI 3.0 or 3.1 JSON document (local $refs only). |
--types | The openapi-typescript output for the same document. |
--config | Configuration module, default slq.codegen.ts. |
--check | Write nothing; exit 1 when any file would change. |
--prune | Delete generated modules the document no longer produces. |
Configuration
// slq.codegen.ts — loaded with Node's own type stripping
import { defineConfig, kebab } from '@svelte-lean/query-codegen';
export default defineConfig({
spec: 'api/openapi.json',
types: 'src/lib/api/schema.gen.ts',
out: (tag) => `src/lib/features/${kebab(tag)}/api/${kebab(tag)}.gen.ts`, // one module per tag
envelope: 'data', // unwrap this property of documented responses (Laravel resources)
untagged: 'System', // module of operations without a tag
names: { 'markets.index': 'listMarkets' }, // operationId or `METHOD path` → export name
exclude: ['admin-db.query'], // written by hand instead
tags: { defaults: true },
typesImport: '$lib/api/schema.gen'
});| Option | Default | Meaning |
|---|---|---|
out | required | Path of a tag’s module; must end in .gen.ts. |
envelope | false | Property unwrapped from 2xx JSON responses; must match the runtime profile. |
untagged | none | Module of operations without a tag. |
names | {} | Export names by operationId or METHOD path. |
entities | {} | Entity tag names ({ one, many }) by path segment. |
exclude | [] | Operations written by hand instead. |
tags.defaults | true | Default entity, tags and invalidates (below). |
typesImport | relative path | Import specifier of the openapi-typescript file. |
Output
// Generated by @svelte-lean/query-codegen from openapi.json (tag "Markets"): endpoints. Do not edit.
import { defineMutation, defineQuery, tag, type } from '@svelte-lean/query';
import type { ListMarketsParams, Market, UpdateMarketInput } from './markets.types.gen';
import * as policy from './markets.policy';
declare module '@svelte-lean/query' {
interface Tags {
Market: number;
Markets: void;
}
interface Permissions {
'markets.view': true;
'markets.edit': true;
}
}
export const listMarkets = defineQuery<'api/markets', Market[], ListMarketsParams>({
get: 'api/markets',
params: type<ListMarketsParams>(),
result: type<Market[]>(),
entity: 'Market', // default tags: GET /markets answers objects with an id
tags: ['Markets'],
...policy.listMarkets // the policy module wins
});
export const updateMarket = defineMutation<'api/markets/{market}', Market, UpdateMarketInput>({
put: 'api/markets/{market}',
body: type<UpdateMarketInput>(),
result: type<Market>(),
invalidates: (_, input) => [tag('Market', input.market), 'Markets'],
access: 'markets.edit', // from x-permission
...policy.updateMarket
});Per tag, two files: <tag>.gen.ts with the endpoints and the registry
augmentation, and <tag>.types.gen.ts with the types and one interface per
operation (ListMarkets { params; result }) for policy modules. No hook names,
no aggregate API object.
| OpenAPI | Generated |
|---|---|
| GET, or POST with x-slq-read: true | defineQuery with get: or post: |
| POST, PUT, PATCH, DELETE | defineMutation with that method |
| path /api/markets/{market} | 'api/markets/{market}', the parameter typed by its schema |
| JSON request body | body: type<Input>() |
| multipart/form-data body | encode: 'multipart'; binary fields typed Blob |
| 2xx JSON response | result: type<R>(), unwrapped by envelope |
| Laravel paginator | result: page<Row>() |
| binary response | result: file() |
| 204 No Content | no result |
| x-permission | access and the Permissions registry |
| 422 responses | nothing: field errors are typed by the body |
Default tags
Names come from the last static path segment (call-requests gives CallRequest and CallRequests).
| Operation | Default |
|---|---|
| GET /x answering a page or an array of objects with id | entity: 'X', tags: ['Xs'] |
| GET /x/{id} answering an object with id | entity: 'X' |
| POST /x where GET /x is such a list | invalidates: ['Xs'] |
| PUT, PATCH, DELETE /x/{id} | invalidates: (_, input) => [tag('X', input.id), 'Xs'] |
| anything else | none: the policy module decides |
Policy modules
// src/lib/features/markets/api/markets.policy.ts — what the document does not say, by hand
import { all, type MutationPolicy, type QueryPolicy } from '@svelte-lean/query';
import type { ListMarkets, UpdateMarket } from './markets.types.gen';
export const listMarkets = {
fresh: '10m',
persist: true,
access: all('markets.view', (params) => params.region !== 'internal')
} satisfies QueryPolicy<ListMarkets['params'], ListMarkets['result']>;
export const updateMarket = {
queue: (input) => `market:${input.market}` // one write per market at a time
} satisfies MutationPolicy<UpdateMarket['input'], UpdateMarket['result']>;What the document does not say (freshness, queues, polling, access beyond x-permission) lives in a hand-written <tag>.policy.ts. The
generated module spreads it last, so the policy wins. Use satisfies, not a type
annotation: an annotated value carries every optional member into the spread and costs more type
checking per endpoint.
Limits
- JSON documents only (convert YAML first); local
$refs only. - Singular and plural names follow English rules;
entitiesandnamesoverride them. - Query parameters of a write travel in the body; a backend that reads them only from the query string needs a hand-written endpoint.
- Request bodies other than JSON and multipart are an error; exclude the operation.
Reference
- @svelte-lean/query-codegen
- Defining endpoints: the shape the generator writes