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

nestjs-hexagonal-backend

Backend development with NestJS, TypeORM, and hexagonal architecture. Use when creating, modifying, or debugging backend code.

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

ayunis-core/ayunis-core스타 34(GitHub 34 + cccskills 0)
연동 도구PostgreSQL

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md11.5 KB

SKILL.md(원문)

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

NestJS Hexagonal Backend Development

Red-Green TDD Workflow

Behavior and logic changes, including bug fixes, follow red-green TDD. Do NOT write production code for changed behavior without a failing test first. For a behavior-preserving refactor, identify and run the focused existing tests before editing, then keep them green; do not invent a new failing test when no behavior should change.

Cycle

  1. Red — Write a test that captures the desired behavior. Run it. It MUST fail.
    • If the test passes immediately, it's not testing anything new — delete it or rethink.
  2. Green — Write the minimum production code to make the test pass. Nothing more.
  3. Refactor — Clean up while keeping tests green. Run the validation required by the repository's Proportional Workflow.
  4. Repeat — Next behavior, next test.
# During each cycle:
pnpm run test -- --testPathPatterns=<module>  # Run focused tests (red → green)

The Jest flag is --testPathPatterns (plural). The singular --testPathPattern was removed and now errors out: Option testPathPattern was replaced by --testPathPatterns.

What Makes a Meaningful Test

  • Test behavior, not implementation — assert on outputs and side effects, not internal method calls.
  • One logical assertion per test — each test proves one thing. Name it after what it proves.
  • Use realistic data — don't use "test", "foo", "bar". Use domain-realistic values.
  • Cover the edges — happy path alone is insufficient. Test error cases, boundary values, empty inputs, duplicates.
  • Tests are documentation — a reader should understand the feature by reading tests alone.
// GOOD ✓ — behavior-focused, descriptive name, realistic data
it("should reject agent creation when name exceeds 200 characters", async () => {
  const longName = "A".repeat(201);
  await expect(useCase.execute({ name: longName })).rejects.toThrow(
    AgentNameTooLongError,
  );
});

// BAD ✗ — tests implementation, vague name, no real assertion
it("should work", async () => {
  const result = await useCase.execute({ name: "test" });
  expect(result).toBeDefined();
});

Test Structure

  • Use cases — test through the public execute() method with stubbed ports.
  • Domain entities — test invariants and business rules directly.
  • Mappers — test round-trip: domain → record → domain preserves all fields.
  • Controllers — only test HTTP-specific concerns (status codes, serialization). Business logic is tested via use cases.

Validation Sequence

Choose validation breadth using the repository's Proportional Workflow.

Fast Path:

pnpm exec eslint <touched-files>              # Lint without rewriting unrelated files

Run pnpm run test -- --testPathPatterns=<module> when behavior or test code changed. Also run pnpm exec tsc --noEmit when types, imports, dependency injection, or other compile-time behavior could be affected.

Standard Path:

pnpm exec eslint <touched-files>     # Lint without rewriting unrelated files
pnpm exec tsc --noEmit               # 0 type errors
pnpm run test -- --testPathPatterns=<module>  # Affected module suites pass

High-Risk changes and changes to shared test infrastructure or broad behavior also run pnpm run test for the full suite.

⚠ pnpm run lint is wired with --fix in ayunis-core-backend/package.json — running it as a verification step will auto-rewrite unrelated files (migrations, fixtures, entities) and leave them in the working tree. Prefer pnpm exec eslint <paths> for verification. If you do run pnpm run lint, immediately check git status --short and git restore any files outside your change set.

When module boundaries or imports change, run dep-cruiser as an architecture cross-check (it catches the no-cross-module-port-imports / no-domain-imports-presenters rules that the pre-commit hook enforces):

pnpm exec depcruise src        # or whatever the project's depcheck script is

Backend-Specific TypeScript Rules

  • strict: true with strictPropertyInitialization: false (for TypeORM entities)
  • noImplicitReturns: true — every code path must return
  • Use Logger (from @nestjs/common) instead of console.*

Module Structure (Hexagonal)

Every domain module ships a SUMMARY.md. Read it first when editing an existing module, and create one when you scaffold a new module — don't submit a PR for a new module without it. The repo convention is uniform: ~96% of existing modules have one, and Bugbot will flag any new module that lands without it.

[module]/
├── SUMMARY.md           # ← Read this first; required for every module
├── domain/              # Pure entities, no decorators
├── application/
│   ├── use-cases/       # Business operations
│   ├── ports/           # Abstract classes (DI tokens, not interfaces)
│   └── dtos/            # Validation decorators
├── infrastructure/
│   └── persistence/postgres/
│       ├── schema/      # TypeORM records
│       ├── mappers/     # Domain ↔ Record conversion
│       └── *.repository.ts
├── presenters/http/     # Controllers (thin)
└── [module].module.ts   # NestJS wiring

Extract module services by responsibility

Keep each class focused on one cohesive responsibility. When a change introduces substantial state, lifecycle management, policy, coordination, caching, pooling, retrying, throttling, or other independently testable behavior, extract it into a dedicated injectable service in the same module instead of growing the existing use case or adapter.

Use cases orchestrate reusable application services. Infrastructure adapters stay focused on translating between an application port and an external technology.

Place the extracted service according to what it owns:

  • application/services/ — reusable application or domain policy consumed by use cases.
  • infrastructure/.../*.service.ts — technical behavior tied to an SDK, transport, process lifecycle, cache, pool, queue, or another external mechanism. Application code depends on an application port rather than importing the concrete infrastructure service directly.

Before extending an existing class, ask:

  1. Does the new behavior have a separate reason to change?
  2. Does it own independent state or lifecycle?
  3. Can it be named and tested independently?
  4. Could more than one operation or use case need it?

If any answer is yes, prefer a dedicated module service, test it directly, and register it with the NestJS module. Keep a small behavior inline only when it is inseparable from the class's primary responsibility.

File & class naming

Suffixes carry structural meaning — the wrong one silently breaks the layer contract readers rely on and triggers review churn. Before naming a new file, grep how the suffix is already used (find src -name "*.<suffix>.ts") and match that role.

  • *.service.ts — application-layer domain service (application/services/). In infrastructure/, only for adapters wrapping an external capability (encryption, export, transcription, queue) — never a persistence read helper.
  • *.repository.ts — port-backed persistence adapter. Must extends an abstract port (every *Repository does). No port → it is not a repository.
  • *.query.ts — query-input DTO in the application layer: the parameter object for a query use case (GetXQuery). Not a persistence read class.
  • *.command.ts — command-input DTO in the application layer: the parameter object for a write use case.
  • *.mapper.ts — object conversion, always in a mappers/ dir. Two roles: domain ↔ record in infrastructure/persistence/*/mappers/, and domain → response DTO in presenters/http/mappers/ (some modules also keep application-layer mappers in application/mappers/).

Ports are abstract classes, not interfaces — NestJS DI needs a runtime token. Impls extends the port; the module wires { provide: Port, useClass: Impl }; consumers inject the abstract class directly (no @Inject('TOKEN')).

A read-only collaborator of a repository (a findOne*/findMany* helper with no port, often extracted to keep the repository under the file-size limit) is neither a repository nor a service — name it *.finder.ts / *Finder.

Key Patterns

Entity IDs — Generated in domain layer, not database

// Domain entity
constructor(id: string | null) {
  this.id = id ?? randomUUID();  // ID generated HERE
}

// Record uses @PrimaryColumn, NOT @PrimaryGeneratedColumn
@PrimaryColumn('uuid')
id: string;

Errors — Domain errors, not HTTP exceptions

// In use case
throw new AgentNotFoundError(agentId); // Domain error

// NOT
throw new NotFoundException(); // ✗ HTTP exception

Database Migrations

For schema changes, use the typeorm-migrations skill. Never write migrations by hand — always auto-generate from entity changes.

Persistence Queries

When creating or modifying repositories, finders, QueryBuilder code, or database calls in loops, load the persistence-query-review skill when it is available. Count the complete call path's database round trips and prefer an atomic database operation when it preserves the same behavior.

CLS transaction enrollment

Repositories and finders reached from @Transactional() methods must resolve reads and writes through the ambient transaction host at operation time. Review all records, subtype repositories, QueryBuilders, raw SQL and synchronous collaborators using the enrollment checklist. Use a real PostgreSQL rollback test for multi-statement atomicity.

Completion Checklist

  • pnpm exec eslint <touched-files> passes (avoid pnpm run lint — it's wired with --fix)
  • Focused regression or module tests pass when behavior or test code changed
  • Type-check, full suite, and dep-cruiser run when required by the Proportional Workflow and affected failure modes
  • git status --short shows only files you intended to change
  • No any types introduced
  • DTOs have validation decorators
  • New entities have proper mappers
  • New module has a SUMMARY.md at its root (the repo convention; missing files are flagged by Bugbot)
  • New stateful or reusable responsibilities are extracted into dedicated module services
  • Changed persistence paths have no avoidable round trips, check-then-act races, or N+1 queries

Anti-Patterns

Don'tWhyInstead
Test implementation details (mock internals)Brittle tests that break on refactorTest inputs → outputs and side effects
Use vague test names (should work, handles error)Tests are documentationName the specific behavior being proven
Throw HTTP exceptions from use casesCouples domain to HTTPThrow domain errors
Name a portless read helper *.repositoryImplies a port-backed adapter*.finder.ts / *Finder

리뷰

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

댓글

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

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

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. 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의 스킬 모두 보기

이 스킬의 문제 신고