Auto-generated GraphQL CRUD, type-safe clients, and React Query hooks from Drizzle PostgreSQL schemas. Full type inference, zero codegen.
bun add graphql-suite The CRUD half of a GraphQL API is the part nobody wants to write and everybody writes anyway. The schema already knows the tables, the columns, the relations and the types — yet you still hand-author resolvers for each one, then hand-author the client types to match, then keep all three in sync forever.
graphql-suite generates that layer from a Drizzle ORM schema and keeps the types flowing through to the React hooks.
Examples
Define the schema once, serve it:
import { buildSchema } from '@graphql-suite/schema'
import { createYoga } from 'graphql-yoga'
import { db } from './db'
const { schema } = buildSchema(db, {
tables: { exclude: ['session', 'verification'] },
})
const yoga = createYoga({ schema })
Query it with types inferred straight from the same Drizzle tables — no codegen step, no generated files to commit:
import { createDrizzleClient } from '@graphql-suite/client'
import * as schema from './db/schema'
const client = createDrizzleClient({ schema, url: '/api/graphql' })
const users = await client.entity('user').query({
select: { id: true, name: true, posts: { id: true, title: true } },
where: { name: { ilike: '%john%' } },
limit: 10,
})
users is typed from the selection, so removing name from select turns every downstream user.name into a compile error.
Why it exists
I started out using drizzle-graphql, which nicely shows the idea works. But I kept missing the things that turn a generated API into one you can actually ship: filtering across relations rather than only top-level columns, hooks to run authorisation before a resolver, count queries for pagination UIs, native JSON/JSONB columns, configurable naming suffixes, self-relations with their own depth limit, and control over which tables get exposed at all.
At some point the wrapper was larger than the thing it wrapped, so it became its own project.
How it’s different
| Approach | What it is |
|---|---|
graphql-suite | generated CRUD from Drizzle, plus a typed client and React hooks in the same package |
| drizzle-graphql | the original idea — schema generation from Drizzle, without relation filtering, hooks or permissions |
| Hasura, PostGraphile | a service in front of the database, with its own metadata, deployment and permission model |
| Hand-written resolvers | total control, and total maintenance |
Use Hasura or PostGraphile if you want a database-facing product with a console and a permissions UI — they do far more than this and are operated as infrastructure. Use hand-written resolvers when your API deliberately does not mirror your tables. This suite fits the case where the API is mostly your schema, and you want it type-safe end to end without running another service.
What’s inside
Three packages, three sets of peer dependencies
| Import | Needs |
|---|---|
@graphql-suite/schema | drizzle-orm, graphql |
@graphql-suite/client | drizzle-orm |
@graphql-suite/query | react, @tanstack/react-query |
A backend never installs React to get the schema builder. The scoped packages are the ones that actually ship — @graphql-suite/schema, @graphql-suite/client and @graphql-suite/query — and the umbrella graphql-suite depends on them under the hood as the convenient, always-consistent way to take all three. A backend-only service can install @graphql-suite/schema on its own and treat it as an extended drizzle-graphql.
Hooks, for the logic generation can’t guess
const { schema, withPermissions } = buildSchema(db, {
hooks: {
user: {
query: {
before: async ({ context }) => {
if (!context.user) throw new Error('Unauthorized')
},
},
},
},
})
Per-role schemas
import { permissive, restricted, readOnly } from '@graphql-suite/schema'
const schemas = {
admin: schema,
editor: withPermissions(permissive('editor', { audit: false, user: readOnly() })),
viewer: withPermissions(restricted('viewer', { post: { query: true } })),
}
Roles are resolved into a schema per role, so an unauthorised field is absent from the schema rather than rejected at execution time.
React Query hooks
The hooks expect both providers above them — TanStack Query’s and this package’s, which carries the client built with @graphql-suite/client:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { GraphQLProvider, useEntity, useEntityList } from '@graphql-suite/query'
const queryClient = new QueryClient()
const App = () => (
<QueryClientProvider client={queryClient}>
<GraphQLProvider client={graphqlClient}>
<UserList />
</GraphQLProvider>
</QueryClientProvider>
)
const UserList = () => {
const user = useEntity('user')
const { data, isLoading } = useEntityList(user, {
select: { id: true, name: true, email: true },
limit: 20,
})
// render data…
}
Caching, pagination and invalidation come from TanStack Query — this layer only supplies the typed keys and fetchers.
Design notes
Type inference is the headline feature. No extra build step, no generated files in review: the client infers queries, filters and result types straight from the Drizzle schema module, so everything just works and stays exactly as type-safe as the schema itself — a table change surfaces as a type error immediately, not as a stale artefact. Codegen exists too (buildSchemaFromDrizzle()), but only for the one case that genuinely needs it: a client in a separate repository that cannot import the schema.
A generated schema pays for depth twice. Deep and self-referencing relations grow the GraphQL schema and the TypeScript types the editor has to instantiate. Depth limits (limitRelationDepth, limitSelfRelationDepth separately for self-relations), per-relation pruning and per-table toggles cut both — a tuned schema comes out up to 90% smaller, and the inferred types stay small enough that the IDE keeps up.
Permissions produce schemas, not guards. Filtering a field at execution time still advertises it in introspection. Building a schema per role means a viewer’s schema genuinely does not contain the fields they cannot read.
One umbrella over three real packages. The scoped packages publish in their own right; the umbrella pins matching versions of all three, so layers generated against the same types cannot drift apart in a lockfile. graphql-suite is the convenient, consistent default — a scoped package is the à-la-carte option.
Status
Live, with a documentation site of its own. PostgreSQL is the supported dialect — the generation leans on Drizzle’s Postgres types, and other dialects would need their own filter mapping rather than a flag.