npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@didactika/prisma-autoread

v1.1.2

Published

Auto-generate Express search endpoints from your Prisma schema — GET/QUERY/POST, Prisma-native filtering, RSQL & OData, aggregations, cursor pagination and HAL/JSON:API/CSV output, zero boilerplate.

Downloads

491

Readme

prisma-autoread

npm version CI License: MIT TypeScript

Node Prisma

Express Fastify Hono

HTTP Formats

Drop-in search endpoints for Express + Prisma. One declaration gives you filtering, sorting, field selection, relation includes, aggregations, pagination (offset and cursor) and multiple response formats — over GET, the new QUERY method, or POST. Query it as if you were writing Prisma.

createAutoRead({ model: 'User', delegate: prisma.user }).applyTo(router);
GET   /users?filter[age][gte]=30&sort=-createdAt&fields=id,firstName&page=2
QUERY /users     { "where": { "age": { "gte": 30 } }, "orderBy": [{ "createdAt": "desc" }] }

Table of contents


Features

  • 🔎 Prisma-native filtering — the query mirrors Prisma's where, coerced and validated against your schema (DMMF). Nothing Prisma can express is lost.
  • 🧬 Multiple input protocols — bracket query strings, JSON bodies, RSQL/FIQL and OData $filter, all producing the same query.
  • 🌐 GET, QUERY & POST — including the new safe/idempotent QUERY method (body-based search).
  • 🧮 Full grammareq/ne/gt/gte/lt/lte/in/notIn/contains/startsWith/endsWith/mode/isNull, AND/OR/NOT, relation some/every/none/is/isNot, and JSON path filters.
  • 🍃 MongoDB embedded documents — filter inside composite type blocks at any depth; is/some wrappers are inserted for you.
  • 📊 Aggregationscount, aggregate (sum/avg/min/max) and group-by.
  • 📄 Pagination — offset (page/limit) and cursor (cursor=), with links.
  • 🎨 Output formats — HAL (default), plain, JSON:API and CSV, with content negotiation.
  • 🛡️ Security — field/relation allow-lists, hidden fields that never reach the response, strict deny-by-default mode, nesting guard.
  • 🔤 Renameable keywords — a column called fields or sort? Rename the control parameter.
  • 🧩 Framework-agnostic — Express, Fastify and Hono bindings (neither is a dependency).
  • 🚀 Fast — O(1) schema lookups, single-pass parsing, optional query-plan cache, telemetry.
  • 🔁 Backward compatible — the original middleware still ships and works unchanged.
  • 📦 Typed — full TypeScript definitions, declared separately in *.d.ts.

📚 Full documentation → — guides, query reference and UML diagrams.


Compatibility

Every combination below is exercised in CI on each push (ci.yml).

| | Supported | Verified in CI | |---|---|---| | Node.js | 20 · 22 · 24 (the three current LTS lines) | full suite on each | | Prisma | 5 · 6 · 7 | unit + integration on all; end-to-end on 5 and 6 | | Express | 4 · 5 | full suite on both | | Fastify | 4 · 5 | binding suite (not a dependency) | | Hono | 4 | binding suite (not a dependency) | | TypeScript | 5.x | build emits CJS + ESM + .d.ts |

  • Express 4 vs 5 — Express 5 changed the default query parser to the flat one, which would break bracket notation. The engine parses the query string itself, so both work with no configuration. (Only the deprecated 0.x middleware, if you mount it directly, needs app.set('query parser', 'extended') on Express 5.)
  • Prisma 7 — Prisma 7 moved the datasource URL out of schema.prisma into prisma.config.ts and requires a driver adapter. That is a Prisma-level requirement of your app; this library reads Prisma.dmmf, which is unchanged, so it works on 5, 6 and 7 alike.
  • Fastify + QUERY — register the method once before binding: fastify.addHttpMethod('QUERY', { hasBody: true }).
  • JSON filtering — advanced JSON filters are only supported by PostgreSQL and MySQL (a Prisma limitation). The JSON path format is detected from your datasource.

Peer dependencies

| Package | Range | |---|---| | @prisma/client | >= 5.0.0 | | express | >= 4.0.0 |

Generate your Prisma Client (npx prisma generate) — the engine reads field and relation metadata from Prisma.dmmf at runtime.


Installation

npm install @didactika/prisma-autoread
npm install @prisma/client express   # peer dependencies

Quick start

import express, { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import { createAutoRead } from '@didactika/prisma-autoread';

const prisma = new PrismaClient();
const app = express();
const router = Router();

createAutoRead({
    model: 'User',
    delegate: prisma.user,               // enables list + count + aggregate + group-by
    methods: ['GET', 'QUERY'],
    routes: ['list', 'count'],
    searchable: ['firstName', 'lastName', 'email'],
}).applyTo(router);

app.use('/users', router);

// Format thrown errors (they carry `.status`).
app.use((err: any, _req: any, res: any, _next: any) => {
    res.status(err.status ?? 500).json({ error: err.message });
});

app.listen(3000);

GET /users now supports filtering, sorting, selection, includes, search and pagination; GET /users/count counts with the same filter.


HTTP methods

Set methods to choose the transports:

| Method | Input | Notes | |---|---|---| | GET | query string | Simple, cacheable, linkable. Default. | | QUERY | JSON body | Safe & idempotent method with a body — for complex queries. | | POST | JSON body | Fallback for clients/proxies that don't speak QUERY. |

GET reads a filter dialect (see below); QUERY/POST read a Prisma-shaped body:

// QUERY /users
{
  "where":   { "age": { "gte": 30 }, "OR": [{ "active": true }, { "age": { "lt": 18 } }] },
  "orderBy": [{ "createdAt": "desc" }],
  "select":  { "id": true, "firstName": true },
  "page": 1, "limit": 20
}

Filtering

On GET with legacy: false, the filter lives under filter[…] and mirrors Prisma:

GET /users?filter[age][gte]=30
GET /users?filter[firstName][contains]=Al&filter[firstName][mode]=insensitive
GET /users?filter[id][in]=1,2,3

Values are coerced to the column type from your schema; unknown fields return 400.

Operators

| Alias | Prisma | | Alias | Prisma | |---|---|---|---|---| | eq / equals | equals | | contains | contains | | ne / not | not | | startsWith / sw | startsWith | | gt gte lt lte | same | | endsWith / ew | endsWith | | in | in | | mode | mode | | nin / notIn | notIn | | isNull | equals: null / not: null |

A bare list is treated as in: filter[id]=1,2{ id: { in: [1, 2] } }.

Logical groups, relations, JSON

# OR / AND / NOT
GET /users?filter[or][0][active]=true&filter[or][1][age][lt]=18
→ { OR: [ { active: true }, { age: { lt: 18 } } ] }

# Relations (to-many auto-wrapped in `some`; explicit some/every/none/is/isNot supported)
GET /users?filter[posts][some][title][startsWith]=Hello
GET /orders?filter[customer][email][contains]=@corp

# MongoDB embedded documents (composite `type` blocks), wrapped in `is` / `some`
GET /course-schedules?filter[program][shortname]=MAT
→ { program: { is: { shortname: 'MAT' } } }
GET /course-schedules?filter[program][subjects][type]=lab
→ { program: { is: { subjects: { some: { type: 'lab' } } } } }

# JSON columns (Prisma-native path filter)
GET /users?filter[metadata][path][0]=theme&filter[metadata][equals]=dark

RSQL and OData

With legacy: false the GET dialects are auto-detected by shape, so these all work on the same endpoint:

# RSQL / FIQL  (; = AND, , = OR, * = wildcard)
GET /users?filter=age=ge=30;name==Al*
GET /users?filter=active==true,age=lt=18
GET /users?filter=role=in=(admin,editor)

# OData $filter
GET /users?$filter=age gt 30 and startswith(name,'Al')
GET /users?$filter=active eq true or age lt 18
GET /users?$orderby=age desc&$select=id,name&$top=20&$skip=40

Restrict the accepted dialects with formats: ['query'] (or ['query','rsql'], …).


Sorting, selection, includes

GET /users?sort=-createdAt,lastName     → orderBy: [{ createdAt: 'desc' }, { lastName: 'asc' }]
GET /users?fields=id,firstName,email     → select: { id, firstName, email }
GET /users?include=posts[comments]       → include posts with nested comments
GET /users?distinct=email                → distinct: ['email']
GET /users?search=alice                  → OR-contains across `searchable` fields

fields (select) and include are mutually exclusive in Prisma — if both are given, select wins.


Pagination

Offset (default):

GET /users?page=2&limit=20

Cursor — pass the last id you saw; the response echoes the next one in pagination.nextCursor and a next link:

GET /users?limit=20&cursor=42

A cursor is a row identifier, not a row number: cursor=42 means "start after the row with id 42", not "start at row 42" — for that, use page/limit. Follow next until it disappears: on the last page nextCursor is absent and hasNext is false. The value is validated against the id column, so a wrong-typed cursor (including a malformed MongoDB @db.ObjectId) returns 400 naming the problem instead of a driver error. Page by another unique column with ?cursor[uuid]=….


Routes

routes accepts a short form or a map with custom paths:

routes: ['list', 'count', 'aggregate', 'groupBy']
routes: { list: true, count: { path: '/total' } }

| Route | Prisma | Default path | Example | |---|---|---|---| | list | findMany (+ count) | / | GET /users?filter[active]=true | | count | count | /count | GET /users/count?filter[active]=true{ "count": 12 } | | aggregate | aggregate | /aggregate | GET /users/aggregate?avg=age&count=true | | groupBy | groupBy | /group-by | GET /users/group-by?by=role&count=true |

Aggregation params: sum, avg, min, max (field lists) and count (true or a field list); group-by adds by and an optional Prisma-native having.


Output formats

hal (default), plain, jsonapi and csv are built in. Pick a default with output, or let clients negotiate:

GET /users?format=csv
GET /users            (Accept: application/vnd.api+json)   → JSON:API

Configuration

createAutoRead(options): { applyTo(router): Router }

| Option | Required | Default | Description | |---|:---:|---|---| | model | ✅ | — | Prisma model name (schema casing). | | delegate | ✅* | — | Prisma model delegate (prisma.user). Enables all routes. | | findByFilter | ✅* | — | Legacy-style callback, alternative to delegate. *One of the two. | | methods | | ['GET'] | HTTP methods to expose. | | routes | | ['list'] | Routes to generate (short form or per-route path map). | | output | | 'hal' | Default output format. | | legacy | | true | true = old GET syntax; false = modern dialects. | | formats | | all | GET dialects when legacy: false (query/rsql/odata). | | searchable | | [] | Fields scanned by ?search=. | | defaults | | {limit:10,maxLimit:100,sort:'id',order:'asc'} | Pagination/sort defaults. | | security | | allow all | { fields, relations, hidden, maxDepth }. | | keywords | | defaults | Rename reserved query params (see below). | | provider / jsonPathSyntax | | auto | JSON path syntax; auto-detected from the datasource. | | cache | | off | true or { max } — cache parsed query plans. | | onQuery | | — | Telemetry hook (t) => void with per-request timings. | | basePathPrefix | | '' | Prefix inserted into generated links. |

Renaming reserved parameters

If a column collides with a control parameter, rename the control — globally once, or per endpoint:

import { Keywords } from '@didactika/prisma-autoread';

Keywords.configure({ fields: 'select', filter: 'q' });   // global, once at bootstrap
createAutoRead({ /* … */, keywords: { limit: 'size' } }); // per endpoint
GET /users?q[age][gte]=30&select=id,firstName&size=20

Frameworks

createAutoRead({ … }).applyTo(expressRouter);   // Express
createAutoRead({ … }).applyToFastify(fastify);  // Fastify
createAutoRead({ … }).applyToHono(honoApp);     // Hono

Fastify and Hono are typed structurally, so neither is a dependency. See docs/frameworks.md.


Security

createAutoRead({
    model: 'User',
    delegate: prisma.user,
    security: {
        fields: ['id', 'firstName', 'email'], // only these are filterable/sortable/selectable
        relations: ['posts'],                 // only these can be traversed/included
        hidden: ['password', 'resetToken'],   // never queryable AND never returned
        maxDepth: 5,                           // reject deeply-nested filters
    },
});

Anything outside the allow-list returns 400. Omit security (or use '*') to allow everything. The policy applies to every dialect, the legacy one included.

fields limits what a client may ask for; it does not change what Prisma returns. For columns that must never leave the server — hashes, tokens, internal flags — use hidden: those are stripped from every response (including rows reached through include, include=* or an embedded document) and rejected in filters, sorts, fields, distinct, group-by and aggregations, worded as if the field did not exist. Dotted paths reach inside: hidden: ['posts.draftNotes'].


Performance

  • DMMF metadata is cached per model as maps → O(1) field/relation lookups.
  • Single-pass parsing builds the Prisma where directly.
  • cache: true memoises parsed query plans by request signature (parsing only — the database is always queried).
  • onQuery gives you { route, format, method, parseMs, execMs, cacheHit }.
  • list reuses its where for the parallel count.

Error handling

Errors carry a .status; register an error handler after your routes:

app.use((err, _req, res, _next) => res.status(err.status ?? 500).json({ error: err.message }));

| Situation | Status | |---|---| | Unknown/again-disallowed field, bad operator, malformed RSQL/OData | 400 | | A backlog capability invoked before it exists | 501 |

An empty result is not an error (200 with data: []).


Migrating from 0.x (legacy)

The original middleware still ships and behaves exactly as before:

import { AutoReadMiddleware, FilterMiddleware } from '@didactika/prisma-autoread';
router.use(FilterMiddleware.processQueryFilters('User'));
AutoReadMiddleware.applyToRouter(router, { modelName: 'User', findByFilter });

To adopt the new engine while keeping the old GET query syntax, just declare it with createAutoRead({ ..., legacy: true }) (the default). When you're ready for the modern grammar, set legacy: false. QUERY/POST bodies are always Prisma-native.


How it works

HTTP (GET | QUERY | POST)
  → Binding          (Express / Fastify / Hono)
    → EndpointController   (framework-agnostic pipeline)
      → InputAdapter       (query / rsql / odata / json / legacy)
        → QueryBuilder     (validate + coerce + map operators, DMMF- and security-aware)
          → QuerySpec      (neutral, Prisma-shaped plan)
            → Route        (list / count / aggregate / group-by)
              → Executor   (findMany / count / aggregate / groupBy)
                → OutputAdapter (hal / plain / jsonapi / csv)

Every input protocol produces the same QuerySpec, and every output format consumes the same result — so adding a protocol, format, route or framework is a self-contained class plus one registration.


Documentation

Full docs live in docs/:

| Guides | Internals | |---|---| | Getting started · Configuration · Query language | Architecture | | Protocols · Output formats · Routes | UML diagrams — context, containers, domain model, use cases, classes, sequences, state | | Keywords · Security · Performance | | | Frameworks · Migration from 0.x | |

14 runnable examples → — from a five-line endpoint to a complete API, covering every framework, protocol and output format.


Contributors

Thanks to everyone who has contributed to this project:

Contributors

Want to help? Read CONTRIBUTING.md.


License

MIT — © Didactika