Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt

Use this file to discover all available pages before exploring further.

Generated wrappers can be emitted with a validator library — zod, valibot, arktype, or zod/mini — as the source of truth. Params are validated on the way in, rows are validated on the way out, and types are derived via the validator’s native inference helper. One definition per thing, no drift. This is an opt-in mode. By default, sqlfu generate emits plain TypeScript types with zero runtime validation.

Why runtime validation at the wrapper boundary

The generator has always made SQL → TypeScript a compile-time guarantee. Runtime validation closes the loop:
  • Bad params fail loudly at the callsite. Mistyped booleans, missing string args, and enum typos throw a readable error before the SQL driver ever sees them.
  • Schema drift surfaces at the adapter boundary. A column removed without regenerating, a newly non-null field, a new enum variant — these become exceptions at the boundary, not silently-wrong objects reaching a React component.
The generated wrapper uses the schemas itself instead of only re-exporting them for consumers. That is the value over plain TypeScript types: every query gets a validated request/response contract by default. The exported schemas are also usable for forms (@rjsf, react-hook-form), tRPC inputs, RPC wire validation, fixtures, and more, but that is a secondary benefit.

Turning it on

sqlfu.config.ts
export default {
  db: './db/app.sqlite',
  migrations: './migrations',
  definitions: './definitions.sql',
  queries: './sql',
  generate: {
    validator: 'zod', // or 'valibot' | 'arktype' | 'zod-mini'
  },
};
After toggling, re-run sqlfu generate. Install the validator library yourself if it is not already a dependency. zod and zod/mini ship as the same package; valibot and arktype are separate packages.

Picking a validator

All four implement Standard Schema, so sqlfu treats them interchangeably. Pick the one whose tradeoffs you already prefer. There is no recommended choice.

zod

Largest API surface and richest ecosystem of downstream integrations (tRPC, react-hook-form, @rjsf). Chainable fluent syntax: z.string().nullable(). Bundle cost is non-trivial on the browser side.

valibot

Smallest runtime, functional composition: v.nullable(v.string()). Best choice when shipping the validator to the browser and keeping the bundle lean.

arktype

TypeScript syntax as schema: type({slug: 'string'}). Strongest editor feedback for complex TS types, zero-runtime-cost inference. Optional fields use the key-suffix ? syntax.

zod-mini

Bundle-optimised subset of zod v4. Same schema primitives as standard zod but a function-call API: z.parse(Schema, input), z.nullable(z.string()). Valibot-sized bundle with zod’s vocabulary.
Whatever you pick, the public shape of the generated wrapper is identical: one callable with .Params, .Result, and .sql attached. You can switch validators later without touching any callsites.

What the generated file looks like

For a query sql/find-post-by-slug.sql:
select id, slug, title, status from posts where slug = :slug limit 1;
The same query renders differently depending on generate.validator:
sql/.generated/find-post-by-slug.sql.ts (generated — do not edit)
import type {Client, SqlQuery} from 'sqlfu';

export type FindPostBySlugParams = {
  slug: string;
};

export type FindPostBySlugResult = {
  id: number;
  slug: string;
  title: string | null;
  status: 'draft' | 'published';
};

const FindPostBySlugSql = `
select id, slug, title, status from posts where slug = ? limit 1;
`;

export async function findPostBySlug(
  client: Client,
  params: FindPostBySlugParams,
): Promise<FindPostBySlugResult | null> {
  const query: SqlQuery = {sql: FindPostBySlugSql, args: [params.slug], name: 'find-post-by-slug'};
  const rows = await client.all<FindPostBySlugResult>(query);
  return rows.length > 0 ? rows[0] : null;
}
Plain types only: no validator import, no runtime checks, no Object.assign namespace wrapper.
The public shape is identical across every validator: one callable, .Params, .Result, .sql. You can swap generate.validator and regenerate without touching any callsites.

Pretty errors

By default (generate.prettyErrors: true), validation failures throw an Error whose message is a readable, indented issues list — one line per issue with the dotted path:
✖ Expected string, received number → at slug
  • Zod uses zod’s native pretty-printer: z.prettifyError(zodError). The generated wrapper calls Params.safeParse(rawParams) and throws new Error(z.prettifyError(error)) on failure. No runtime helper is imported from sqlfu.
  • Arktype, valibot, zod-mini share the Standard Schema ~standard.validate(input) entry point. The generated wrapper inlines the result-guard and, on failure, calls prettifyStandardSchemaError (re-exported from sqlfu) to build the thrown Error’s message. That is the only thing imported from sqlfu in pretty-errors mode.
prettifyStandardSchemaError is re-exported from sqlfu as a reference implementation — a small function you’re welcome to copy and adapt. The stable contract is the Standard Schema Result shape, not sqlfu’s prettifier.

prettyErrors: false

Set prettyErrors: false to let the raw error from the underlying validator library pass through untouched. Choose this if you have error-handling middleware that already introspects the validator’s issues list structurally.
  • Zod emits Params.parse(rawParams) directly. A ZodError propagates unchanged with .issues on it.
  • Arktype, valibot, zod-mini emit an inline check on the Standard Schema result. On failure: throw Object.assign(new Error('Validation failed'), {issues: result.issues}). You still get the issues array on error.issues, without running it through the prettifier.
sqlfu.config.ts
generate: {
  validator: 'zod',
  prettyErrors: false, // default: true
},
In prettyErrors: false mode with any Standard Schema validator, the generated file has zero runtime dependency on sqlfu. Only type Client and type SqlQuery are imported, both erased at compile time.

One identifier per query

The function name (camelCase, matching the SQL filename) is the single identifier for everything related to the query:
import {findPostBySlug} from './sql/.generated/find-post-by-slug.sql.js';

// Call it.
const post = await findPostBySlug(client, {slug: 'hello'});
//    ^? findPostBySlug.Result | null

// Inferred types.
type P = findPostBySlug.Params; // { slug: string }
type R = findPostBySlug.Result; // { id: number; slug: string; title: string | null; status: 'draft' | 'published' }

// Runtime schemas (for forms, RPC, fixtures, etc.).
const schema = findPostBySlug.Params;
const result = findPostBySlug.Result;

// The raw SQL text.
const queryText = findPostBySlug.sql;
Namespace merging is what makes findPostBySlug.Params resolve as a value (the schema) and a type. Consumers do not have to think about this: they write findPostBySlug.Params in either position.

Error behavior and recovery

Validation throws on invalid input. Callers who want recovery can call the schemas directly via each library’s safe-parse equivalent:
const parsed = findPostBySlug.Params.safeParse(userInput);
if (!parsed.success) return handleError(parsed.error);
// parsed.data is the validated value
The wrapper throwing by default is intentional. This is generated code, and the right default is to fail loudly at the boundary.

Not emitting a validator

If generate.validator is unset, null, or undefined, the generator emits the plain TypeScript output: no validator import, no .parse() calls, types declared directly. There is no hybrid mode — a project picks one.

Extending the generated shape

The generated file is readable and small. If you need a specific validator refinement (e.g. .url(), .email(), custom refinements) for a column, wrap the generated function in your application code:
import {findPostBySlug as rawFindPostBySlug} from './sql/.generated/find-post-by-slug.sql.js';
import {z} from 'zod';

const RichParams = rawFindPostBySlug.Params.extend({
  slug: z.string().regex(/^[a-z0-9-]+$/),
});

export async function findPostBySlug(client: Client, params: z.infer<typeof RichParams>) {
  return rawFindPostBySlug(client, RichParams.parse(params));
}
Generated schemas will never be richer than what a SQL type system can tell us. Column-level refinement is an application concern. Pluggable validators and per-column overrides are planned but not yet in scope.

Build docs developers (and LLMs) love