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

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/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:

```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](https://www.prisma.io/docs/orm/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](https://www.prisma.io/docs/orm/middleware/how-middleware-works) covers.

## The rules [#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()` or `updateAll()`                                 | none, because TypeScript makes you write `.where(...)` first |
| 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`, which is why the two are spelled differently in the examples here.

This is the record `lints` builds for a `warn` finding: the code of the rule, a message, and the table the query touched.

```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 [#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 [#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 [#raw-sql]

A whole statement you wrote yourself with [`db.raw.sql`](https://www.prisma.io/docs/orm/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()`](https://www.prisma.io/docs/orm/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 produces a finding, go and read the query yourself before you change anything.

> [!WARNING]
> A severity you set in `severities` applies to a finding by its code wherever that finding came from, so it changes the raw defaults as well as the four rules. If you set `selectStar: 'warn'` to quiet the ORM-API rule, raw SQL's `select *` drops from `error` to `warn` along with it, and `select * from "user"` starts running instead of being blocked. To keep the ORM-API rule at `error` and still run a raw `select *`, give the code that runs it 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 [#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. In a query builder callback the 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`. On a table whose primary key is `id`, `IS NOT NULL` is true for every row:

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

That delete passes the rule and runs, because the 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 it. An `UPDATE` takes the same shape, as in `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 [#see-also]

* [How middleware works](https://www.prisma.io/docs/orm/middleware/how-middleware-works)
* [Built-in: budgets](https://www.prisma.io/docs/orm/middleware/built-in-budgets) for row-count and latency ceilings
* [Authoring custom middleware](https://www.prisma.io/docs/orm/middleware/authoring-custom-middleware)

## Related pages

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