Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Built-in: lints (/docs/orm/middleware/built-in-lints)

For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending .md to its URL.

The lints middleware inspects each query's structure before it runs and blocks or warns on risky shapes.

Location: ORM > Middleware > Built-in: lints

The lints middleware catches queries that are almost certainly a mistake before they reach your database. A DELETE with no WHERE clause removes every row in the table, which is rarely what anyone meant to write, so lints looks at the structure of each query first and either blocks it or logs a warning.

You turn it on by adding it to the middleware list when you create your db client. The default rules are useful as they are, so there is nothing to configure to get started:

src/prisma/db.ts
import postgres from '@prisma/orm-postgres/runtime';
import { lints } from '@prisma/orm-postgres/family-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL']!,
  middleware: [lints()],
});

The built-in middleware is imported from @prisma/orm-postgres/family-runtime, which works with any SQL database. There is nothing extra to install, because it is part of the @prisma/orm-postgres package you already have.

This is the smallest src/prisma/db.ts that works, and the middleware option is the only part specific to lints. The db.ts that create-prisma generates has more in it, including a connectDatabase export that the generated seed.ts imports, so add the middleware option to that file rather than replacing it. Its postgres(...) calls only run when the app is started with DATABASE_URL exported, as npm run dev does. With npm run dev:composer, the generated file takes its client from Composer through service.load() instead, and the middleware option in db.ts is never used, so lints is not on. contract.json and contract.d.ts are the two files npx prisma contract emit writes from your contract, which is the Prisma ORM 8 name for your schema. If you set your project up by hand, The data contract covers where those two files come from.

You register lints on a client you created with postgres(...). Putting lints() in the middleware array of a MongoDB client is a TypeScript error. If you register other middleware alongside it, Prisma ORM runs them in the order you list them in the array, which How middleware works covers.

Every query you make is checked, reads and writes alike, and a query that a rule blocks never reaches your database. The rules work on queries you build with the ORM API or the SQL query builder. Raw SQL that you wrote yourself is checked a different way, described under Raw SQL.

Every rule has a severity, which is warn or error and nothing else. A rule set to error throws, and the query is blocked before it reaches the database. A rule set to warn logs a warning and lets the query run. There is no third value that switches a rule off, so the quietest you can make a rule is warn.

Rule Code severities key Default severity Fires when
DELETE without WHERE LINT.DELETE_WITHOUT_WHERE deleteWithoutWhere error A DELETE has no WHERE clause
UPDATE without WHERE LINT.UPDATE_WITHOUT_WHERE updateWithoutWhere error An UPDATE has no WHERE clause
No LIMIT LINT.NO_LIMIT noLimit warn A SELECT has no LIMIT clause
SELECT star LINT.SELECT_STAR selectStar warn A query selects all columns instead of naming them

[!NOTE] You cannot yet give the postgres(...) client a logger of your own, so a rule set to warn produces nothing you can see: nothing is printed, and there is no callback of yours for it to reach. That makes LINT.DELETE_WITHOUT_WHERE and LINT.UPDATE_WITHOUT_WHERE the two rules you can act on today, because a blocked query is something you notice, and it leaves the other two as rules to plan for until the client accepts a logger.

The rules are named after the SQL they are about, and you write your queries in the ORM API, so here is which of your own calls sets off which rule.

Your call Rule it sets off
.all() with no .limit(...) before it LINT.NO_LIMIT
.first(...) none, because it adds LIMIT 1 for you
a query where you never called .select(...) LINT.SELECT_STAR
deleteAll(), deleteAndCount(), updateAll(), or updateAndCount() after a .where(...) with a condition in it none
deleteAll(), deleteAndCount(), updateAll(), or updateAndCount() after .where({}) LINT.DELETE_WITHOUT_WHERE or LINT.UPDATE_WITHOUT_WHERE
the query builder's delete() or update() with no where() LINT.DELETE_WITHOUT_WHERE or LINT.UPDATE_WITHOUT_WHERE

So await db.orm.public.User.limit(50).all() sets off LINT.SELECT_STAR, while await db.orm.public.User.select('id', 'email').limit(50).all() does not. In db.orm.public.User, public is the PostgreSQL schema and User is your model name. The SQL query builder reaches the same table by its name in the database, as db.sql.public.User. These examples assume a User model with no @@map, so the table has the same name as the model.

Today a warn rule has no visible effect. lints builds a record like the one below, with the code of the rule, a message, and the table the query touched, and passes it to the client's logger, but the postgres(...) client has no logger you can set yet, as the note above says, so the record is thrown away.

Lint warning
warn {
  code: 'LINT.NO_LIMIT',
  message: 'Unbounded SELECT may return large result sets',
  details: { table: 'User' }
}

A rule set to error throws before the database sees anything, so the call you were awaiting rejects. It throws an ordinary Error with four extra properties beside its message: code, category, severity, and details. Use the isRuntimeError type guard, exported from @prisma/orm-postgres/components/runtime, so that TypeScript knows those properties are there and you can read code off the error.

The blocked DELETE below comes from the SQL query builder, where you build the query yourself, call .build(), and run the result with db.runtime().query(...) for rows or db.runtime().execute(...) for a count:

TypeScript
import { isRuntimeError } from '@prisma/orm-postgres/components/runtime';

try {
  const plan = db.sql.public.User.delete().build();
  await db.runtime().execute(plan);
} catch (error) {
  if (isRuntimeError(error) && error.code === 'LINT.DELETE_WITHOUT_WHERE') {
    // add a where() clause, or use a client with the rule set to warn
  }
  throw error;
}

Those properties hold this for a DELETE with no WHERE on the User table:

Blocked DELETE
{
  code: 'LINT.DELETE_WITHOUT_WHERE',
  category: 'LINT',
  severity: 'error',
  message: 'DELETE without WHERE clause blocks execution to prevent accidental full-table deletion',
  details: { table: 'User' }
}

When a default does not suit your project, severities lets you make any rule stricter or gentler. You name only the rules you want to change, using the key from the table of rules, and every rule you leave out keeps its default. Each key takes 'warn' or 'error'. In a service where every list endpoint has to paginate, for example, you can make a SELECT without a LIMIT fail rather than warn, and fail a query that names no columns as well. This is the middleware line of the db.ts above, with the bare lints() replaced:

src/prisma/db.ts (excerpt)
middleware: [
  lints({
    severities: {
      noLimit: 'error',
      selectStar: 'error',
    },
  }),
],

With selectStar set to error, a plain .all() in the ORM API throws, and you catch it the same way you catch a blocked DELETE:

TypeScript
import { isRuntimeError } from '@prisma/orm-postgres/components/runtime';

try {
  await db.orm.public.User.limit(50).all();
} catch (error) {
  if (isRuntimeError(error) && error.code === 'LINT.SELECT_STAR') {
    // name the columns you want with .select(...)
  }
  throw error;
}

A whole statement you wrote yourself with db.raw.sql is SQL text rather than a query Prisma ORM assembled, so the four rules above are skipped for it. lints searches the SQL text instead, every time a raw statement runs, and it starts from its own defaults, which are not the defaults in the table of rules:

What it searches the text for Code Default severity in raw SQL
a select statement containing select * LINT.SELECT_STAR error
a select statement with no limit anywhere in it LINT.NO_LIMIT warn
a statement that changes data in a query marked read-only LINT.READ_ONLY_MUTATION error

So a select * you wrote by hand is blocked:

TypeScript
const plan = db.raw.sql`select * from "User"`
  .returnsRow({ id: 'pg/int4@1' })
  .build();
await db.runtime().query(plan);

It throws an error whose code is LINT.SELECT_STAR and whose message is Raw SQL plan selects all columns via *. returnsRow tells Prisma ORM the type of each column the statement gives back, written as pg/ plus the PostgreSQL type name plus @1, and fns.raw and .returns() lists the ids you can use. The severities key for the last rule is readOnlyMutation, and there is no documented way to mark a query read-only, so you are unlikely to see that one.

Because these are searches through the text rather than checks on the structure of the query, they can be wrong in both directions. The word limit anywhere in the statement counts as a LIMIT, including inside a string or a column name, and a select * inside a subquery counts for the whole statement. So when a raw statement sets off one of these checks, go and read the query yourself before you change anything.

[!WARNING] One severities key sets both the rule for the ORM API and the SQL query builder and the raw SQL check that has the same code, so it changes the raw defaults as well. selectStar is warn by default for the ORM API and the SQL query builder, but error for raw SQL. So setting selectStar: 'warn' changes nothing for the ORM API, which is already at warn: its only effect is that raw SQL's select * drops from error to warn, and select * from "User" starts running instead of being blocked. Setting selectStar: 'error', as under Configure severities, keeps a raw select * blocked. No setting makes the ORM-API rule error and lets a raw select * run, so if you need both, give the code that runs the raw select * a db client of its own, in the way Common gotchas does for a backfill script.

You will not need fallbackWhenAstMissing, because every query from the ORM API, the SQL query builder, and db.raw.sql already has the structure the rules read. Leave it at its default of 'raw'. The only other value it takes turns the checks off for a query that arrives without that structure:

src/prisma/db.ts (excerpt)
middleware: [lints({ fallbackWhenAstMissing: 'skip' })],

[!WARNING] Sometimes you do mean to change every row, in a backfill script for example. LINT.DELETE_WITHOUT_WHERE and LINT.UPDATE_WITHOUT_WHERE are error by default, so lints blocks that write, and a script like that needs one of the two ways through.

The first way is to write a WHERE clause that is true for every row, which states the intent in the query itself. On a table whose primary key is id, id IS NOT NULL is true for every row, and in the ORM API you write it with isNotNull():

scripts/backfill/delete-users.ts
await db.orm.public.User.where((u) => u.id.isNotNull()).deleteAndCount();

An empty filter does not work for this. .where({}) adds no condition, so db.orm.public.User.where({}).deleteAll() sends a DELETE with no WHERE, and lints blocks it with LINT.DELETE_WITHOUT_WHERE.

With the SQL query builder, the callback's first argument, f, has one property for each column, and the second, fns, holds the comparison helpers. Comparing a column to null gives you a null check rather than a comparison, so fns.ne(f.id, null) gives id IS NOT NULL and fns.eq(f.id, null) gives id IS NULL:

TypeScript
const plan = db.sql.public.User
  .delete()
  .where((f, fns) => fns.ne(f.id, null))
  .build();
await db.runtime().execute(plan);

Both deletes pass the rule and run, because each query has a WHERE clause and that is all LINT.DELETE_WITHOUT_WHERE looks for, so you can leave lints on its defaults in the script that runs them. An UPDATE takes the same shape, as in db.orm.public.User.where((u) => u.id.isNotNull()).updateAndCount({ name: 'x' }) or db.sql.public.User.update({ name: 'x' }).where((f, fns) => fns.ne(f.id, null)).build().

The second way is to build that script its own db client with the rule set to warn:

scripts/backfill/db.ts
import postgres from '@prisma/orm-postgres/runtime';
import { lints } from '@prisma/orm-postgres/family-runtime';
import type { Contract } from '../../src/prisma/contract.d';
import contractJson from '../../src/prisma/contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL']!,
  middleware: [lints({ severities: { deleteWithoutWhere: 'warn' } })],
});

Keep that client inside the script and import your usual db everywhere else, because setting the rule to warn for your whole application gives up the protection everywhere.

  • Authoring custom middleware: Build, register, and run your own Prisma ORM middleware, step by step, starting with a query logger.
  • Built-in: budgets: The budgets middleware caps row counts and reports queries that took too long.
  • Built-in: cache: The cache middleware serves repeated reads from an in-memory store, opted in per query with a cache annotation.
  • How middleware works: Middleware runs your code before and after every query, so one policy can cover your whole app.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu