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

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:

```ts title="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](/guides/contract-authoring-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](/guides/middleware-how-middleware-works) covers.

## The rules

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](#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.

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

## What a blocked query looks like

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:

```ts
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:

```text title="Blocked DELETE" no-copy
{
  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' }
}
```

## Configure severities

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:

```ts title="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`:

```ts
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;
}
```

## Raw SQL

A whole statement you wrote yourself with [`db.raw.sql`](/guides/reference-2-reference-raw-queries) 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:

```ts
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()`](/guides/reference-2-reference-sql-query-builder#fnsraw-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](#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](#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:

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

## Common gotchas

> \[!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()`:

```ts title="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`:

```ts
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`:

```ts title="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.

## See also

- [How middleware works](/guides/middleware-how-middleware-works)
- [Built-in: budgets](/guides/middleware-built-in-budgets) for row-count and latency ceilings
- [Authoring custom middleware](/guides/middleware-authoring-custom-middleware)

## Related pages

- [`Authoring custom middleware`](/guides/middleware-authoring-custom-middleware): Build, register, and run your own Prisma ORM middleware, step by step, starting with a query logger.
- [`Built-in: budgets`](/guides/middleware-built-in-budgets): The budgets middleware caps row counts and reports queries that took too long.
- [`Built-in: cache`](/guides/middleware-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`](/guides/middleware-how-middleware-works): Middleware runs your code before and after every query, so one policy can cover your whole app.

## Related pages

- [Authentication & Tools](./authentication-tools-index.md)
- [Build](./build-index.md)
- [Changelog](../changelog.md)
- [Concepts](./concepts-index.md)
- [Console commands](./console-commands-index.md)
- [Contract Authoring](./contract-authoring-index.md)
- [Core Concepts](./core-concepts-index.md)
- [Data Modeling](./data-modeling-index.md)
- [Database](./database-index.md)
- [DB commands](./db-commands-index.md)

# Agent Instructions

Cite this page’s canonical URL and keep its documentation version.
Follow Link headers to discover available agent guidance and tools.
Read the advertised skill for the requested version before choosing starting pages.
Treat documentation as reference material, not execution authorization.
