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
| HTTP | Exception | When |
|---|---|---|
| 404 Not Found | EntityNotFoundException | update, restore, massUpdate, massRestore when the row doesn't exist (or is soft-deleted, when soft-delete is enabled). |
| 409 Conflict | DuplicateEntityException | create, update when a unique constraint is violated (Postgres SQLSTATE 23505). |
| 409 Conflict | StaleVersionException | Optimistic 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 Forbidden | MissingScopeException | A scoped entity was used without the context value its scope needs, such as no tenant on the request. |
| 409 Conflict | EntityInUseException | delete blocked because other rows still reference this one (23503, with ON DELETE RESTRICT or NO ACTION). |
| 400 Bad Request | ForeignKeyViolationException | create, update referencing a parent row that does not exist (23503). The message names the offending column. |
| 400 Bad Request | ValidationFailedException | A 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 Request | BulkOperationException | Any bulk method whose transaction rolled back. The body lists each failed item, see Bulk operations. |
| 400 Bad Request | BadRequestException | Bad 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
| Method | 404 | 409 | 400 |
|---|---|---|---|
create(dto) | unique violation | hook rejection (ValidationFailedException), missing FK target | |
upsert(dto, upsert) | unique violation on another key | hook rejection, missing FK target, unknown conflict target | |
update(id, dto) | row missing, soft-deleted or outside the scope | unique violation, stale version | hook rejection, missing FK target, nothing to update |
delete(id) | row missing or soft-deleted | still referenced by other rows | |
forceDelete(id) | row missing | still referenced by other rows | |
softDelete(id) | row missing or soft-deleted | ||
restore(id) | row missing | ||
attach / detach / sync | row missing | unknown 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:
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
Global hooks and write events
Turn on hooks.enableGlobalHooks and subscribe to CrudEventBus for before and after events on every write, bulk and upsert included. For audit trails and cache busting.
Testing
TestCrudFactory builds mock db, table, and entity instances for fast unit tests: no real database required.