From nestjs-clean-arch
Activates when the user asks to throw an exception, handle error, add error handling, throw not found, throw forbidden, throw bad request, or inject exception service in a NestJS Clean Architecture project. Guides correct use of IException interface with proper injection, error codes, and module wiring.
npx claudepluginhub tuannguyen151/foxdemon-plugins --plugin nestjs-clean-archThis skill uses the workspace's default tool permissions.
The exception system separates the exception contract (domain layer) from the HTTP implementation (infrastructure layer). Use cases throw exceptions through the `IException` interface; the infrastructure layer maps those to HTTP responses.
Provides error handling patterns, exception filters, error boundaries, logging, retries, and recovery strategies for React, Next.js, and NestJS apps. Use for async errors, monitoring integration, and graceful degradation.
Implements standardized API error handling with RFC 7807 responses, typed error classes, middleware, and monitoring. Use for consistent HTTP errors across endpoints.
Provides NestJS patterns for scalable Node.js/TypeScript backends: modular architecture, dependency injection, DTO validation, repositories, and events.
Share bugs, ideas, or general feedback.
The exception system separates the exception contract (domain layer) from the HTTP implementation (infrastructure layer). Use cases throw exceptions through the IException interface; the infrastructure layer maps those to HTTP responses.
// src/domain/exceptions/exceptions.interface.ts
export interface IFormatExceptionMessage {
type: string // short error code, e.g. 'TASK_NOT_FOUND'
message: string // human-readable message
}
export const IException = Symbol('IException')
export interface IException {
badRequestException(data: IFormatExceptionMessage): never
internalServerErrorException(data?: IFormatExceptionMessage): never
forbiddenException(data?: IFormatExceptionMessage): never
unauthorizedException(data?: IFormatExceptionMessage): never
notFoundException(data?: IFormatExceptionMessage): never
}
The concrete implementation lives at src/infrastructure/exceptions/exceptions.service.ts and is exported by ExceptionsModule (src/infrastructure/exceptions/exceptions.module.ts).
| Method | HTTP Status | When to Use |
|---|---|---|
notFoundException | 404 | Resource not found by id or lookup key |
badRequestException | 400 | Invalid input, constraint violation, business rule failure |
forbiddenException | 403 | Authenticated but not permitted to perform the action |
unauthorizedException | 401 | No valid authentication (missing or invalid JWT) |
internalServerErrorException | 500 | Unexpected server-side failures |
Import both the interface and the Symbol token. Inject via @Inject(IException) — never instantiate ExceptionsService directly.
import { Injectable, Inject } from '@nestjs/common'
import { ITaskRepository } from '@domain/repositories/task.repository.interface'
import { IException } from '@domain/exceptions/exceptions.interface'
@Injectable()
export class DeleteTaskUseCase {
constructor(
@Inject(ITaskRepository) private readonly taskRepository: ITaskRepository,
@Inject(IException) private readonly exception: IException,
) {}
async execute({ id, userId }: { id: number; userId: number }): Promise<void> {
const task = await this.taskRepository.findOneTask({ id, userId })
if (!task) {
this.exception.notFoundException({
type: 'TASK_NOT_FOUND',
message: `Task with id ${id} not found`,
})
}
await this.taskRepository.deleteTask(id)
}
}
Key rules:
this.exception.<method>(...) directly — no throw keyword required; the return type is never.type as an ALL_CAPS snake_case error code scoped to the domain (e.g. TASK_NOT_FOUND, INVALID_STATUS, USER_ALREADY_EXISTS).message for a human-readable explanation that can be shown to API consumers.Import ExceptionsModule in the feature module so that IException is resolvable in the DI container.
// src/modules/task.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { Task } from '@infrastructure/databases/postgresql/entities/task.entity'
import { ITaskRepository } from '@domain/repositories/task.repository.interface'
import { IException } from '@domain/exceptions/exceptions.interface'
import { TaskRepository } from '@infrastructure/databases/postgresql/repositories/task.repository'
import { ExceptionsModule } from '@infrastructure/exceptions/exceptions.module'
import { ExceptionsService } from '@infrastructure/exceptions/exceptions.service'
import { DeleteTaskUseCase } from '@use-cases/tasks/delete-task.use-case'
@Module({
imports: [
TypeOrmModule.forFeature([Task]),
ExceptionsModule, // ← registers and exports ExceptionsService
],
providers: [
{ provide: ITaskRepository, useClass: TaskRepository },
{ provide: IException, useClass: ExceptionsService }, // ← bind Symbol → implementation
DeleteTaskUseCase,
],
})
export class TaskModule {}
ExceptionsModule exports ExceptionsService, so importing the module is sufficient. The explicit provider binding ({ provide: IException, useClass: ExceptionsService }) makes the Symbol token resolvable for @Inject(IException) in use cases.
Chain additional guards using the same injected service:
async execute(dto: CreateTaskDto & { userId: number }): Promise<Task> {
if (!dto.title?.trim()) {
this.exception.badRequestException({
type: 'INVALID_TASK_TITLE',
message: 'Task title must not be empty',
})
}
const existing = await this.taskRepository.findTaskByTitle(dto.title, dto.userId)
if (existing) {
this.exception.badRequestException({
type: 'TASK_TITLE_DUPLICATE',
message: `A task with title "${dto.title}" already exists`,
})
}
return this.taskRepository.createTask({ ...dto })
}
Provide a partial mock with only the methods called by the use case under test:
import { IException } from '@domain/exceptions/exceptions.interface'
const mockException = {
notFoundException: jest.fn(),
badRequestException: jest.fn(),
forbiddenException: jest.fn(),
unauthorizedException: jest.fn(),
internalServerErrorException: jest.fn(),
}
const module = await Test.createTestingModule({
providers: [
DeleteTaskUseCase,
{ provide: ITaskRepository, useValue: mockTaskRepository },
{ provide: IException, useValue: mockException },
],
}).compile()
Assert that the correct method was called with the expected payload:
it('throws notFoundException when task does not exist', async () => {
mockTaskRepository.findOneTask.mockResolvedValue(null)
await useCase.execute({ id: 1, userId: 42 })
expect(mockException.notFoundException).toHaveBeenCalledWith({
type: 'TASK_NOT_FOUND',
message: 'Task with id 1 not found',
})
})
| Pattern | Example |
|---|---|
<ENTITY>_NOT_FOUND | TASK_NOT_FOUND, USER_NOT_FOUND |
<ENTITY>_<FIELD>_<REASON> | TASK_TITLE_DUPLICATE, USER_EMAIL_INVALID |
INVALID_<FIELD> | INVALID_STATUS, INVALID_DATE_RANGE |
<ENTITY>_<ACTION>_FORBIDDEN | TASK_DELETE_FORBIDDEN |
Keep error codes scoped to the domain, not to HTTP verbs or implementation details.
IException Symbol from @domain/exceptions/exceptions.interface@Inject(IException) private readonly exception: IExceptionthis.exception.<method>({ type, message }) inside the use case execute() methodExceptionsModule in the feature module's imports array{ provide: IException, useClass: ExceptionsService } in the feature module's providerstype error codes{ provide: IException, useValue: { notFoundException: jest.fn(), ... } } in tests