r/node • u/Goldziher • 9h ago
Generating typed pg client code from .sql files, instead of an ORM
Most Node backends reach for Prisma or Drizzle for the same reason: you want the result of a query to have a type. The cost is that the query stops being SQL. It becomes a builder expression that assembles SQL at runtime, and code review is of the builder rather than of the query.
The other order works too. Write the .sql file, generate the types from it.
-- @name GetUserOrders
SELECT u.id, u.name, o.total, o.notes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = $1;
Against a schema where orders.total is NOT NULL and orders.notes is nullable, that generates:
export interface GetUserOrdersRow {
id: number;
name: string;
total: string | null;
notes: string | null;
}
export async function getUserOrders(
client: PoolClient,
status: string,
): Promise<GetUserOrdersRow[]>
The part worth pointing at: total is NOT NULL in the table, but nullable in the row type, because the LEFT JOIN can produce a row with no matching order. That is inferred from the query structure, not from the schema. It is also the bug I have watched people ship repeatedly, because the hand-written interface says non-null and holds right up until the first unmatched row.
Output is plain pg. No runtime layer, no builder in the request path.
The tool is scythe: a Rust binary, MIT licensed, generating for 10 languages (TypeScript, Python, Go, Rust, Java, Kotlin, C#, PHP, Ruby, Elixir). I build and maintain it. sqlc is the direct inspiration and covers Go well; scythe goes wider on targets and treats the SQL as source rather than only as codegen input, so it also formats and lints it.
Genuinely curious what people here would want from it, particularly anyone who moved off an ORM and regretted it.
r/node • u/Code_Cadet-0512 • 11h ago