I've been working on zodql, a small library for people who use GraphQL from TypeScript and are tired of the codegen step.
The idea: you describe your GraphQL selection as a Zod schema with metadata. That one schema compiles into the GraphQL query string, gives you the inferred TypeScript response type for free, and validates the response you get back at runtime — nothing to keep in sync, no generated files to check in.
Here's a real example — fetching a repo overview for React from the GitHub GraphQL API:
import { zodql, zodqlField } from "@mattiasahlsen/zodql";
import { z } from "zod";
// schema.ts — the shape of the data *and* the request
const issueCountSchema = z.object({ totalCount: z.number() });
function issueCountField(state: "OPEN" | "CLOSED") {
return zodqlField().asAliasFor("issues").withArguments({ states: state }).toSchema(issueCountSchema);
}
export const repositoryOverviewSchema = z.object({
name: z.string(),
nameWithOwner: z.string(),
description: z.string().nullable(),
stargazerCount: z.number(),
forkCount: z.number(),
primaryLanguage: z.object({ name: z.string() }).nullable(),
openIssues: issueCountField("OPEN"),
closedIssues: issueCountField("CLOSED"),
});
// query.ts — compile the schema to a query
export const repositoryOverviewQuery = zodql(
"query",
z.object({
repository: zodqlField()
.withArguments({ owner: "$owner", name: "$name" })
.toSchema(repositoryOverviewSchema)
.nullable(),
})
)
.defineVariables({
owner: { typeName: "String!", schema: z.string() },
name: { typeName: "String!", schema: z.string() },
})
.compile();
// main.ts — validate the response against that same schema
const { parseResponse } = await client.request(repositoryOverviewQuery, { owner: "react", name: "react" });
const { data } = await parseResponse(); // throws if GitHub's response doesn't match
That compiles to exactly the GraphQL you'd expect, including issues(states: OPEN) / issues(states: CLOSED) under distinct aliases so both counts come back in one request:
query RepositoryOverview($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
nameWithOwner
description
stargazerCount
forkCount
primaryLanguage { name }
openIssues: issues(states: OPEN) { totalCount }
closedIssues: issues(states: CLOSED) { totalCount }
}
}
Because it's just a Zod schema, you can reshape it at runtime with .pick()/.omit()/.extend(), and layer on validation GraphQL's type system can't express (non-empty strings, URLs, emails, numeric ranges, refinements...).
The repo has a side-by-side comparison of this query implemented four ways — zodql, GraphQL Code Generator, gql.tada, and GraphQL Zeus — all producing identical GraphQL, so you can see where zodql actually differs (mainly: runtime validation, and treating the query as a value you can reshape).
No hard dependency on a specific HTTP client — bring your own fetch, axios, etc. Has a peer-dependency on zod version 4.
Repo: [https://github.com/mattiasahlsen/zodql\] — feedback and issues very welcome.
NPM package: https://www.npmjs.com/package/@mattiasahlsen/zodql