Drizzle Resource
Typed query layer for Drizzle ORM

One contract.
Every table
endpoint.

drizzle-resource gives your server a typed query layer for filters, search, sorting, pagination, row hydration, and facets, all inferred from your Drizzle schema.

$ npm install drizzle-resource
orders.ts
const 
result
= await
orders
.
query
({
context
: {
orgId
: "acme" },
request
: {
context
: {},
pagination
: {
mode
: "offset",
pageIndex
: 1,
pageSize
: 25 },
sorting
: [{
key
: "createdAt",
dir
: "desc" }],
search
: {
value
: "laptop",
fields
: [] },
filters
: [
{
type
: "condition",
key
: "status",
operator
: "isAnyOf",
value
: ["pending", "processing"],
}, ],
facets
: [{
key
: "status",
mode
: "exclude-self",
limit
: 10 }],
}, });

Validation integrations

Keep the resource contract
at the transport boundary

Derive strict request and relation-aware response schemas from the same resource your query runs. Zod and Valibot overrides keep their column inputs and parsed output types intact, so endpoint validation does not introduce a second contract to maintain.

Explore validation
Zod
Use requestSchema and responseSchema with Zod. Parse nested relations and preserve transformed output through z.infer.
Valibot
Use the same resource contract with Valibot. Column overrides stay typed from input through v.InferOutput.

The problem it solves

Every table API ends up with the same ad-hoc logic: parse sort params, build WHERE clauses, run a count, hydrate rows, maybe group buckets for a filter sidebar. You rewrite it slightly differently each time, and the client has to learn a different shape each time. drizzle-resource standardizes that entirely.

Staged pipeline

Not a monolithic query

Every request runs five ordered stages — watch one flow through. Any stage can be replaced independently.

  1. 01Scope merge

    Tenant filters merge in before any client filter can run.

  2. 02Field validation

    Sort keys and filter paths are checked against your schema.

  3. 03ID select

    One paginated primary-key query with every filter applied.

  4. 04Row hydration

    Rows load by id with declared relations, order preserved.

  5. 05Facets

    Bucket counts resolve only when the request asks for them.

Define once

The resource is
the contract

Declare relations, scope, search, sort, and facet policy next to your schema. Every endpoint speaks the same request shape — the server decides what is allowed.

  • Scope filters merge server-side — clients cannot bypass tenancy
  • Unknown sort keys and filter paths are rejected before any SQL runs
  • The request shape never changes from one table to the next
export const 
ordersResource
=
engine
.
defineResource
("orders", {
relations
: {
customer
: true,
orderLines
: {
with
: {
product
: true } },
},
query
: {
scope
: (
f
,
ctx
) =>
f
.
is
("customer.orgId",
ctx
.
orgId
),
search
: {
allowed
: ["reference", "customer.name", "orderLines.product.name"],
defaults
: ["reference", "customer.name"],
},
sort
: {
defaults
: [{
key
: "createdAt",
dir
: "desc" }] },
facets
: {
allowed
: ["status", "customer.name", "orderLines.product.category"],
}, }, });
Filters
Nested AND/OR trees with 11 operators — is, isAnyOf, contains, between, before, after, and more.
Search
Free-text matching across allowed field paths, with separate allowed and defaults lists per resource.
Sorting
Multi-column with per-resource defaults. Disable expensive paths until they are benchmarked.
Pagination
1-based page index and page size, with configurable defaults.
Facets
Bucket counts for filter sidebars, traveling in the same payload as the main query.
Scope
Context filters merged into every query before client input. Tenancy as a structural guarantee.

Granular execution

Four methods, one pipeline

resource.query()

The full pipeline — ids, rows, and optional facets in one call.

resource.queryIds()

Page ids and pagination metadata only — cache or batch the rest.

resource.queryRows()

Hydrate a known id list without re-running selection.

resource.queryFacets()

Resolve facet buckets independently from the page.

Admin & data tables
One request shape drives filter bars, column sorting, search, pagination, and row selection. No bespoke API per table.
Faceted explorer UIs
Facet requests travel in the same payload as the main query. No parallel API needed.
IDs-first pipelines
Cache or delay row hydration by splitting into queryIds and queryRows. The contract stays identical.
Multi-tenant APIs
Scope filters merge before any client filter and cannot be bypassed. Tenancy is a structural guarantee.
Tunable performance
Start with automatic execution. Replace only the expensive stage when benchmarks show a bottleneck.
Fully typed field paths
Field paths inferred from your schema and relations. Typos in sort keys or filter fields are compile-time errors.

Ready to standardize
your table APIs?

Define your engine, add a resource, run your first query.

::

Copyright © 2026