Nestjs Drizzle
Guides

Errors

HTTP status code mapping for every exception thrown by SqlBaseCrudService, with the per-method reference.

Every exception in nestjs-drizzle-crud extends @nestjs/common's HttpException. Nest's default exception filter maps the subclass to an HTTP status code automatically. No configuration required. You can instanceof the exports in a custom exception filter and the mapping will hold.

Status code table

HTTPExceptionWhen
404 Not FoundEntityNotFoundExceptionupdate, restore, massUpdate, massRestore when the row doesn't exist (or is soft-deleted, when soft-delete is enabled).
409 ConflictDuplicateEntityExceptioncreate, update when a unique constraint is violated (Postgres SQLSTATE 23505).
409 ConflictStaleVersionExceptionOptimistic locking: update, softDelete, restore or delete named a version the row is no longer at. In a bulk call it is a 409 entry of the 400.
403 ForbiddenMissingScopeExceptionA scoped entity was used without the context value its scope needs, such as no tenant on the request.
409 ConflictEntityInUseExceptiondelete blocked because other rows still reference this one (23503, with ON DELETE RESTRICT or NO ACTION).
400 Bad RequestForeignKeyViolationExceptioncreate, update referencing a parent row that does not exist (23503). The message names the offending column.
400 Bad RequestValidationFailedExceptionA value the column cannot take: too long (22001), a failed check (23514), out of range (22003), the wrong type such as "abc" for an integer or an unknown enum label (22P02), or a bad date (22007, 22008), or the same key twice in one massUpsert (21000). Also an update with no column to set, a column outside writableColumns, and whatever your validateCreate / validateUpdate hooks throw.
400 Bad RequestBulkOperationExceptionAny bulk method whose transaction rolled back. The body lists each failed item, see Bulk operations.
400 Bad RequestBadRequestExceptionBad pagination, sort, filter or search input: an unknown filter column or operator, an operator on the wrong column type, an invalid date, an invalid cursor, a findOne with no condition, an unknown fullTextSearch column, anything outside an allowlist, and anything parseQuery cannot read. See Pagination & sorting, Clamping & fail-fast.

DatabaseConnectionException and TransactionException are still exported but deprecated: nothing in the package throws them. A failed transaction rejects with the error that caused it.

All of them are re-exported from the package root. See Exports.

Per-method reference

Reads: never throw EntityNotFoundException

find, findOne, findAll, exists, count, and fullTextSearch return null / false / empty for missing rows. They never throw EntityNotFoundException. Callers decide how to respond.

Bad input on a read is a 400, the same as on a write. find('abc') on an integer key, findAll({ status: 'archived' }) when the enum has no such label, or { createdAt: { gte: 'garbage' } } all return 400 instead of a raw 500.

A "missing" row includes soft-deleted rows when soft-delete is enabled.

Writes: when each method throws

Method404409400
create(dto)unique violationhook rejection (ValidationFailedException), missing FK target
upsert(dto, upsert)unique violation on another keyhook rejection, missing FK target, unknown conflict target
update(id, dto)row missing, soft-deleted or outside the scopeunique violation, stale versionhook rejection, missing FK target, nothing to update
delete(id)row missing or soft-deletedstill referenced by other rows
forceDelete(id)row missingstill referenced by other rows
softDelete(id)row missing or soft-deleted
restore(id)row missing
attach / detach / syncrow missingunknown target ids (404 entries), not a many-to-many

On a scoped entity, a row of another tenant counts as missing everywhere: a 404 on a write, null on a read.

restore returns the restored row (Postgres RETURNING); update returns the updated row.

Bulk methods

Every bulk method (massCreate, massUpdate, massSoftDelete, massRestore, massDelete, massForceDelete, massUpsert) runs in a single transaction. Any failure throws BulkOperationException (→ 400) and rolls back the entire operation. No partial writes. The response body lists each failed item as { index | id, status, message }, and the exception's errors field keeps the original errors for logging.

executeSqlTransaction

Wraps a callback in a transaction. On any throw inside the callback, the transaction rolls back and the same error propagates unchanged. Pass { transaction: tx } in the options of any CRUD method to enlist it.

Custom exception filters

Because the exceptions extend HttpException, custom filters only need to handle the typed subclass:

exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import {
  DuplicateEntityException,
  EntityNotFoundException,
} from 'nestjs-drizzle-crud';

@Catch(EntityNotFoundException, DuplicateEntityException)
export class CrudExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();

    if (exception instanceof DuplicateEntityException) {
      res.status(409).json({
        statusCode: 409,
        error: 'Conflict',
        field: exception.field,
        value: exception.value,
      });
      return;
    }

    if (exception instanceof EntityNotFoundException) {
      res.status(404).json({
        statusCode: 404,
        error: 'Not Found',
        entity: exception.entity,
        id: exception.id,
      });
      return;
    }
  }
}

If you don't write a custom filter, Nest's default filter already maps the subclass to the status code. You get correct 404 / 409 / 400 / 500 out of the box.

Next

On this page