sveltelean Query
Version v0.2.0

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-typescript

Run

Terminal
# 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 --check

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

FlagMeaning
--specOpenAPI 3.0 or 3.1 JSON document (local $refs only).
--typesThe openapi-typescript output for the same document.
--configConfiguration module, default slq.codegen.ts.
--checkWrite nothing; exit 1 when any file would change.
--pruneDelete generated modules the document no longer produces.

Configuration

slq.codegen.ts
// 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'
});
OptionDefaultMeaning
outrequiredPath of a tag’s module; must end in .gen.ts.
envelopefalseProperty unwrapped from 2xx JSON responses; must match the runtime profile.
untaggednoneModule 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.defaultstrueDefault entity, tags and invalidates (below).
typesImportrelative pathImport specifier of the openapi-typescript file.

Output

src/lib/features/markets/api/markets.gen.ts
// 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.

OpenAPIGenerated
GET, or POST with x-slq-read: truedefineQuery with get: or post:
POST, PUT, PATCH, DELETEdefineMutation with that method
path /api/markets/{market}'api/markets/{market}', the parameter typed by its schema
JSON request bodybody: type<Input>()
multipart/form-data bodyencode: 'multipart'; binary fields typed Blob
2xx JSON responseresult: type<R>(), unwrapped by envelope
Laravel paginatorresult: page<Row>()
binary responseresult: file()
204 No Contentno result
x-permissionaccess and the Permissions registry
422 responsesnothing: field errors are typed by the body

Default tags

Names come from the last static path segment (call-requests gives CallRequest and CallRequests).

OperationDefault
GET /x answering a page or an array of objects with identity: 'X', tags: ['Xs']
GET /x/{id} answering an object with identity: 'X'
POST /x where GET /x is such a listinvalidates: ['Xs']
PUT, PATCH, DELETE /x/{id}invalidates: (_, input) => [tag('X', input.id), 'Xs']
anything elsenone: the policy module decides

Policy modules

src/lib/features/markets/api/markets.policy.ts
// 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; entities and names override 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