Catch N+1 queries in your tests, before they hit production.
An ORM-agnostic query-count assertion library for TypeScript. Prisma, Drizzle, and more.
Quickstart | Why | ORM support | API | Roadmap
queryward asserts how many SQL queries your ORM runs inside a test, so an N+1 fails CI instead of production. Ruby has bullet, Python has nplusone, PHP has phpunit-query-count-assertions. TypeScript had nothing. queryward fills that gap.
Note
Early but working. The core (query collection, fingerprinting, N+1 detection) and the assertion API are tested and green. ORM adapters are wired; the CI budget mode and the first npm release are in progress. See the roadmap.
npm i -D querywardimport { drizzle } from "drizzle-orm/node-postgres";
import { querywardLogger } from "queryward/drizzle";
import { assertNoNPlusOne, assertQueryCount } from "queryward";
const db = drizzle(client, { logger: querywardLogger });
test("listing posts stays within budget", async () => {
await assertQueryCount(() => getPostsWithAuthors(db), { max: 2 });
await assertNoNPlusOne(() => getPostsWithAuthors(db));
});A function that looks innocent in review:
const posts = await db.query.posts.findMany();
for (const post of posts) {
// one extra query per post: the classic N+1
post.author = await db.query.users.findFirst({ where: eq(users.id, post.authorId) });
}queryward turns it into a failing test:
QuerywardError: Detected N+1: 50x of the same query shape.
SELECT * FROM users WHERE id = ?
Total queries: 51
50x SELECT * FROM users WHERE id = $1
1x SELECT * FROM posts
One join later, the test is green at 2 queries. The regression can never reach production again.
N+1 is the most common backend performance bug, and today it is caught by luck: a reviewer's eye, or a production dashboard after the incident. Every other ecosystem has a test-time safety net for it. TypeScript did not.
| Ecosystem | Test-time N+1 safety net |
|---|---|
| Ruby | bullet, prosopite |
| Java | hibernate-query-asserts, QuickPerf |
| Python | nplusone, Django assertNumQueries |
| PHP | phpunit-query-count-assertions |
| TypeScript | queryward |
queryward is a devDependency. It never touches your production code path.
- Automatic N+1 detection from repeated query shapes.
- Query budgets you assert in tests with
assertQueryCount(fn, { max }). - ORM-agnostic through thin adapters. Your tests read the same no matter the ORM.
- Vitest and Jest matchers:
await expect(fn).toHaveNoNPlusOne(). - Readable diagnostics: the exact query shape, how many times, and which budget it broke.
- Zero production footprint: dev-only, no runtime middleware, no agent, no dashboard to host.
| ORM | Status |
|---|---|
| Drizzle | ✅ supported (reference adapter) |
| Prisma | ✅ supported |
| Kysely | 🛠 planned |
| TypeORM | 🛠 planned |
| Sequelize | 🛠 planned |
Adapters are tiny. Contributing a new one is the easiest way to help, see the roadmap.
- An ORM adapter (a logger or a query-event hook) reports each SQL statement into the active measurement scope, tracked with Node's
AsyncLocalStorage. - queryward normalizes each statement into a structural fingerprint, stripping literal values so
WHERE id = 1andWHERE id = 2collapse to the same shape. - A shape repeated past a threshold is an N+1. A total over your budget fails the assertion, with a diagnostic that points straight at the offending query.
measureQueries(fn): Promise<QueryReport> // { count, queries, shapes, nPlusOne }
assertQueryCount(fn, { max }): Promise<QueryReport>
assertNoNPlusOne(fn, { threshold? }): Promise<QueryReport>Adapters: queryward/drizzle (querywardLogger), queryward/prisma (attachPrisma). Matchers: import "queryward/vitest".
CI query-budget mode (queryward snapshot and queryward check), Jest matchers, more ORM adapters, EXPLAIN-based full-scan detection, and the first npm release. Full list in ROADMAP.md.
Issues and pull requests are welcome, especially new ORM adapters. Get started with npm install then npm test.
Standing on the shoulders of bullet, prosopite, and nplusone, which proved how valuable a test-time N+1 net can be.
MIT © Yeongjun Yoo