본문으로 건너뛰기
cccskills
무료GitHub에 공개

use-case-reference

Reference implementation for backend use cases — error handling, structure, and patterns. MUST be loaded when creating or modifying any *.use-case.ts file.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core스타 34(GitHub 34 + cccskills 0)

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md9.8 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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';
// Error-boundary decorator — exact import path varies by project
import { HandleUnexpectedErrors } from 'src/common/decorators/handle-unexpected-errors.decorator';
// Module-specific errors
import { EntityNotFoundError, UnexpectedEntityError } from '../../entity.errors';
// Repository class — the type doubles as the DI token (no @Inject() needed)
import { EntityRepository } from '...';

interface DoSomethingCommand {
  entityId: string;
  // ... other command fields
}

@Injectable()
export class DoSomethingUseCase {
  private readonly logger = new Logger(DoSomethingUseCase.name);

  constructor(
    private readonly entityRepository: EntityRepository,
    // ... other dependencies (other use cases, application services, ports)
  ) {}

  // Error handling — REQUIRED on every execute(), see Rule 1
  @HandleUnexpectedErrors(UnexpectedEntityError)
  async execute(command: DoSomethingCommand): Promise<Entity> {
    this.logger.log({ entityId: command.entityId }, 'Doing something');

    // 1. Auth context — handling varies by project, see Rule 6 below

    // 2. Precondition checks (existence, permissions, business rules)
    // (multi-tenant projects typically pass userId here for tenant isolation)
    const entity = await this.entityRepository.findOne(command.entityId);
    if (!entity) {
      throw new EntityNotFoundError(command.entityId);
    }

    // 3. Business logic — mutate, orchestrate, call other use cases / repos
    //    (style varies: rich domain methods like entity.updateName(...), or
    //    anemic record updates — follow your project's convention)

    // 4. Persist and return
    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

// WRONG ✗ — couples domain to HTTP
throw new UnauthorizedException('User not authenticated');
throw new NotFoundException('Entity not found');

// CORRECT ✓ — domain errors
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. Extract broader responsibilities into application services

A use case represents one operation; it must not become the home for a broader capability. Extract cohesive policy, coordination, caching, batching, throttling, lifecycle management, or other independently testable and reusable behavior into a dedicated injectable service in the module.

Place reusable application behavior in application/services/ and inject that service into each consuming use case. If the behavior is a technical mechanism tied to infrastructure, define an application port and keep the concrete service in infrastructure; use cases must not import concrete adapters.

Extract the responsibility when it has its own reason to change, owns state or lifecycle, can be named and tested independently, or could serve more than one operation. Keep code inline only when it is inseparable from that single use case.

constructor(
  private readonly policyService: EntityPolicyService,
  private readonly externalCapability: ExternalCapabilityPort,
) {}

5. 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.

6. 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, orgId } = getRequiredUserContext(this.contextService);
    

    Ayunis Core centralizes this in src/common/context/required-context.ts: getRequiredUserContext(context) for user-only operations and getRequiredOrgId(context) for operations that also accept API-key principals. Both throw UnauthorizedAccessError on missing context — do not hand-roll a get(...) + if (!x) throw pair. Plain optional get(...) stays correct where missing context is a supported state.

  • 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.

7. Validate preconditions before mutating

Always check existence and permissions before performing writes:

// (multi-tenant projects typically pass userId for tenant isolation)
const entity = await this.repository.findOne(id);
if (!entity) {
  throw new EntityNotFoundError(id);
}
// Only then proceed with mutation

8. Each module has its own errors file

Errors live in a module-specific errors file (e.g. application/<module>.errors.ts):

// ApplicationError import path varies by project
import { ApplicationError } from '...';

export enum EntityErrorCode {
  ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
  UNEXPECTED_ENTITY_ERROR = 'UNEXPECTED_ENTITY_ERROR',
  // ... other codes
}

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) {
    // If your project's `ApplicationError` accepts a metadata object as a 4th
    // arg (some do, some don't), pass `{ error }` for context.
    super('Unexpected error occurred', EntityErrorCode.UNEXPECTED_ENTITY_ERROR, 500);
  }
}

Every module MUST have an Unexpected*Error class for the @HandleUnexpectedErrors decorator.

9. Logger — use the class name, log entry, metadata first

private readonly logger = new Logger(MyUseCase.name);

// At the start of execute():
this.logger.log({ relevantId: command.id }, 'Descriptive action');

Metadata goes in the first argument. Nest's logger treats the last argument as the context, so logger.log('Descriptive action', { relevantId }) silently drops the object instead of emitting structured fields.

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:

  • execute() is decorated with @HandleUnexpectedErrors(Unexpected*Error)
  • No hand-written try/catch error boundary in execute()
  • No HTTP exceptions (NotFoundException, UnauthorizedException, etc.)
  • Auth context handled per the project's convention (Rule 6)
  • Preconditions checked before mutations (entity exists, permissions valid)
  • Repositories injected via DI token (interface), not concrete class
  • Broader reusable responsibilities are delegated to application services or ports
  • Use cases do not import concrete infrastructure services or adapters
  • Module has an Unexpected*Error class in its errors file
  • Logger uses class name, logs entry point with metadata first (error logging is the decorator's job)

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Query Ayunis deployed-environment telemetry in AppSignal — incidents, occurrence counts and error traces via the `appsignal-cli` binary; structured log attributes (user.id, org.id, nestjs.context, custom metadata) via the v2 logs API. Use when investigating a production/staging incident or log data — NOT for local dev logs (see backend-debugging).

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

Ayunis Core backend specifics — module boundaries, key files, and project conventions. Complements the nestjs-hexagonal-backend skill.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

Frontend development in ayunis-core. Use when creating, modifying, or debugging frontend code (React, Feature-Sliced Design, API client).

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

Debug backend runtime errors (500s, crashes, unexpected behavior). Use when something is broken at runtime — not for writing new code.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

Shared test fixtures for backend specs — port mock factories, domain builders, and shared IDs in a per-module testing/ folder. Use when a module's *.use-case.spec.ts files repeat the same mock-repository literal, inline entity construction, or hardcoded UUIDs across files.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

Analyze Cursor Bugbot PR comments through a root-cause lens: is the finding valid, and is the suggested fix the real fix or a bandaid over a deeper problem? Use when the user asks to check, assess, or triage bugbot comments.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ayunis-core/ayunis-core342026년 10월 26일 업데이트

ayunis-core의 스킬 모두 보기

이 스킬의 문제 신고