# Codegen

> Installing @svelte-lean/query-codegen, the endpoint modules it generates from an OpenAPI document, and the policy modules that complete them.

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

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.

> **Note** `@svelte-lean/query-codegen` is experimental: the API can still change in a minor release. Stability per entry point: [Entry points](https://svelte-lean.vercel.app/query.md#entry-points).

## Install

A development dependency, with [openapi-typescript](https://openapi-ts.dev) for the types. Node `>=22.18.0`: the configuration file is TypeScript, loaded with Node's own type stripping.

```bash
npm install --save-dev @svelte-lean/query-codegen openapi-typescript
```

## Run

```bash
# 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.

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

```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'
});
```

| 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

`src/lib/features/markets/api/markets.gen.ts`

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

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

```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 `$ref`s 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

- [@svelte-lean/query-codegen](https://svelte-lean.vercel.app/query/api/codegen.md)
- [Defining endpoints](https://svelte-lean.vercel.app/query/endpoints.md): the shape the generator writes
