| name | use-case-reference |
| description | Reference implementation for backend use cases — error handling, structure, and patterns. MUST be loaded when creating or modifying any *.use-case.ts file. |
Use Case Reference Implementation
This skill defines the canonical use case structure. Every use case MUST follow this pattern.
The structural rules (error-boundary decorator, error wrapping, single execute() method, repository injection via interface) are non-negotiable. Things that vary by project — exact import paths, auth context handling, domain model style — are marked with // ... placeholders and inline comments.
Structure
import { Injectable, Logger } from '@nestjs/common';
import { HandleUnexpectedErrors } from 'src/common/decorators/handle-unexpected-errors.decorator';
import { EntityNotFoundError, UnexpectedEntityError } from '../../entity.errors';
import { EntityRepository } from '...';
interface DoSomethingCommand {
entityId: string;
}
@Injectable()
export class DoSomethingUseCase {
private readonly logger = new Logger(DoSomethingUseCase.name);
constructor(
private readonly entityRepository: EntityRepository,
) {}
@HandleUnexpectedErrors(UnexpectedEntityError)
async execute(command: DoSomethingCommand): Promise<Entity> {
this.logger.log('Doing something', { entityId: command.entityId });
const entity = await this.entityRepository.findOne(command.entityId);
if (!entity) {
throw new EntityNotFoundError(command.entityId);
}
return await this.entityRepository.save(entity);
}
}
Rules
1. Every execute() method MUST be decorated with @HandleUnexpectedErrors
@HandleUnexpectedErrors(UnexpectedEntityError)
async execute(command: DoSomethingCommand): Promise<Entity> {
The decorator (from src/common/decorators/handle-unexpected-errors.decorator.ts — exact path varies by project) is the use case's error boundary:
- Re-throws
ApplicationError subclasses as-is — these are domain errors with proper status codes
- Logs unexpected errors under the use-case class name — no extra context needed, the class name already describes the operation
- Wraps unexpected errors in the module-specific
Unexpected*Error — never let raw errors escape
Do NOT hand-write try/catch error boundaries in execute(). A try/catch inside the business logic is fine only when the use case genuinely handles a failure (fallback, retry) rather than translating it.
2. Never throw HTTP exceptions from use cases
throw new UnauthorizedException('User not authenticated');
throw new NotFoundException('Entity not found');
throw new UnauthorizedAccessError();
throw new EntityNotFoundError(entityId);
Use cases throw ApplicationError subclasses. The global exception filter converts them to HTTP responses.
3. One operation per file, one execute() per use case
A use case is a single business operation. Don't bundle multiple operations into one class with execute1() / execute2(). If you need two operations, write two use cases.
4. Inject repositories via the repository class — never database clients directly
Use cases depend on a repository class. They MUST NOT import a database client (TypeORM, Drizzle, raw pg, etc.) directly. This keeps the use case testable without a real database.
constructor(
private readonly entityRepository: EntityRepository,
) {}
Whether EntityRepository is an abstract class with a separate concrete implementation bound via { provide: EntityRepository, useClass: ConcreteRepository } (port/adapter pattern), or a single concrete class registered directly in providers: [EntityRepository], is a project-level decision — see your project's structural conventions skill. In both cases the use case constructor looks the same: TypeScript reflection picks up the class as the DI token, so no @Inject() decorator is needed.
5. Auth context — varies by project
Auth handling is a project-level convention. Common patterns:
-
ContextService / async-local-storage: read the current user from a request-scoped context service
const userId = this.contextService.get('userId');
if (!userId) throw new UnauthorizedAccessError();
-
Command parameter: the controller (or a guard) injects userId into the command before calling the use case
-
Guard + decorator: an auth guard runs before the controller and rejects unauthenticated requests; use cases assume auth has already passed
Whichever pattern your project uses, apply it consistently inside execute(). Never accept userId ad-hoc in some use cases and not others.
6. Validate preconditions before mutating
Always check existence and permissions before performing writes:
const entity = await this.repository.findOne(id);
if (!entity) {
throw new EntityNotFoundError(id);
}
7. Each module has its own errors file
Errors live in a module-specific errors file (e.g. application/<module>.errors.ts):
import { ApplicationError } from '...';
export enum EntityErrorCode {
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
UNEXPECTED_ENTITY_ERROR = 'UNEXPECTED_ENTITY_ERROR',
}
export abstract class EntityError extends ApplicationError {
constructor(message: string, code: EntityErrorCode, statusCode: number = 400) {
super(message, code, statusCode);
}
}
export class EntityNotFoundError extends EntityError {
constructor(entityId: string) {
super(`Entity with ID ${entityId} not found`, EntityErrorCode.ENTITY_NOT_FOUND, 404);
}
}
export class UnexpectedEntityError extends EntityError {
constructor(error: unknown) {
super('Unexpected error occurred', EntityErrorCode.UNEXPECTED_ENTITY_ERROR, 500);
}
}
Every module MUST have an Unexpected*Error class for the @HandleUnexpectedErrors decorator.
8. Logger — use the class name, log entry
private readonly logger = new Logger(MyUseCase.name);
this.logger.log('Descriptive action', { relevantId: command.id });
Unexpected-error logging is handled by the @HandleUnexpectedErrors decorator — do not add your own error logging for the boundary.
Checklist
When creating or modifying a use case, verify: