30

30-backend-service-module-pattern

A standardized pattern for organizing complex backend services using action registries, status machines, and mirrored tests.

Install

mkdir -p .claude/skills/30-backend-service-module-pattern && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15020" && unzip -o skill.zip -d .claude/skills/30-backend-service-module-pattern && rm skill.zip

Installs to .claude/skills/30-backend-service-module-pattern

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Canonical pattern for new backend service modules in `app/backend/src/services/<domain>/<archetype>/` — action registry, status machine, pure naming helpers, external-API client, background poller, and mirrored Jest tests. Load this skill when introducing a new multi-verb domain service, replacing a legacy `switch (action)` block in `routes/`, or adding a sibling archetype to an existing service.
399 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Intermediate

Key capabilities

  • Organize backend service modules with action registries
  • Implement status machines for domain services
  • Create external-API clients
  • Develop background reconciliation loops
  • Structure mirrored Jest tests for services
  • Migrate legacy route handlers incrementally

How it works

This pattern organizes backend service modules into a consistent structure, including an action registry, status machine, and external API clients. It handles requests by dispatching actions based on the request body, allowing for incremental migration from legacy route handlers.

Inputs & outputs

You give it
A request body with an 'action' field
You get back
A processed response from the service module

When to use 30-backend-service-module-pattern

  • Refactor complex route handlers
  • Add new backend domain services
  • Implement background reconciliation loops

About this skill

Backend Service Module Pattern

Reference implementation: app/backend/src/services/deployment/docker/ Mirrored tests: app/backend/src/tests/services/deployment/docker/

When to apply this pattern

Use it when a route handler in app/backend/src/routes/ dispatches on a body.action (or similar verb) field across more than ~3 branches, or when a single domain owns multiple verbs plus a background reconciliation loop. Do not apply it to simple CRUD endpoints — those stay inline in the route file.

Pre-requisites

  • A spec or design note (e.g., under specs/<NNN>-<slug>/) listing the action verbs, status enum, and external systems involved. Action handlers and tests reference its FR / US / AS identifiers inline (see existing JSDoc headers for shape).
  • The legacy route handler exists and is currently in routes/functions.ts or a sibling file. The cutover is incremental — the registry's fallback keeps legacy verbs working until each verb is migrated.

Directory layout

app/backend/src/services/<domain>/<archetype>/
├── types.ts                   # Status enum + predicates, action verb union, context interface, row shape, error taxonomy
├── <archetype>Service.ts      # Entry point: action registry + `handle(req,res,body,fallback)`
├── statusMachine.ts           # Pure guards + custom error classes (e.g., ConcurrentDeployError)
├── naming.ts                  # Pure, deterministic resource-name helpers
├── <external>Client.ts        # Thin wrapper over fetch/SDK (one per external system)
├── poller.ts                  # Optional: background reconciliation loop
├── _armContext.ts             # Optional: shared sub-context builder, underscore-prefixed
└── actions/
    ├── _failure.ts            # Shared internal helpers — underscore-prefixed, not exported from index
    ├── create.ts              # Exports `createAction(ctx): Promise<void>`
    ├── deploy.ts
    ├── destroy.ts
    ├── status.ts
    └── …                      # One file per verb (small verb families may share a file, e.g. start/stop/restart → lifecycleArm.ts)

Mirror exactly under app/backend/src/__tests__/services/<domain>/<archetype>/. Each source file has a matching .test.ts.

Required conventions

  1. File header JSDoc — every file opens with: purpose, the legacy block it replaces (if any), spec/FR/US references, and an @example. Match the style of actions/create.ts.
  2. Action registry entry point — see dockerDeploymentService.ts:
    • actions: Partial<Record<ActionVerb, Handler>> populated by imports at top of file.
    • WIRE_ALIASES: Record<string, ActionVerb> for legacy wire names (e.g., deletedestroy). Apply at the boundary so the frontend wire format is unchanged.
    • handle(req, res, body, fallback) resolves the verb, dispatches, or invokes fallback() for verbs not yet migrated.
    • Export register…Action, _reset…ForTests, _getRegistered… (underscore = test-only).
  3. Handler signatureexport async function <verb>Action(ctx: <Archetype>Context): Promise<void>. Handlers respond directly via ctx.res and return void; they never throw to the registry for expected error paths.
  4. Context shape — defined once in types.ts:
    export interface <Archetype>Context {
      req: Request;
      res: Response;
      body: Record<string, unknown> & { action: <ActionVerb>; <primaryId>: string; … };
    }
    
  5. Status machinetypes.ts exports the status string-literal union plus TRANSITIONAL / TERMINAL ReadonlySets and isTransitional / isTerminal predicates. statusMachine.ts exports assertCanAccept<Action>(currentStatus) returning hints (e.g., { clearFailureAttrs: boolean }) and a custom Error subclass carrying the offending status. See statusMachine.ts.
  6. Pure helpersnaming.ts and similar pure modules take a plain options object, return a plain result object, and contain zero I/O. They are pinned by exhaustive it.each([...]) tables in naming.test.ts.
  7. External client — one <external>Client.ts per external system (e.g., genappWorkflowClient.ts). Takes the auth token as a parameter; never reads credentials itself. Exports narrow …Params interfaces alongside each function. Module-level const reads env vars with sensible defaults documented in a comment.
  8. Shared failure helpers — extract repeated UPDATE … status='failed' … + broadcast blocks into actions/_failure.ts. See actions/_failure.ts for recordFailure and formatDispatchHttpCause.
  9. Logging — every module declares const LOG_PREFIX = "[<domain>-<archetype>:<verb>]" and logs through utils/logger, never console.
  10. Broadcasts — DB status changes are followed by broadcast(\<channel>-${projectId}`, "<event>", payload)fromutils/websocket` so the frontend receives push updates.
  11. Poller (when applicable) — selects transitional rows, processes each in a per-row transaction wrapped in pg_try_advisory_xact_lock(hashtextextended(id, 0)) for multi-replica safety. Exports tick…Poller() for deterministic test ticks plus start…Poller() / stop…Poller() for index.ts. See poller.ts.
  12. No coupling to the legacy file — the new module MUST NOT import from routes/functions.ts. The only bridge is the runtime fallback closure passed to handle().

Test conventions (Jest)

Each handler test in __tests__/services/<domain>/<archetype>/actions/<verb>.test.ts follows the create.test.ts shape:

  • jest.mock() every I/O dependency at the top: utils/database, utils/logger, utils/rpcHelpers, utils/githubAuth, websocket, the external client.
  • A makeCtx(overrides?) factory that returns { ctx, res } where res.status and res.json are chainable jest.fn().mockReturnThis().
  • One describe per handler; it titles reference the spec/FR/US (e.g., "persists resource names before dispatch (FR-002)").
  • For state-machine and naming modules, use parametric it.each([...]) tables to cover every enum × action pair.
  • Reset registries between tests with _reset…ForTests() to avoid cross-test bleed.

Incremental cutover procedure

  1. Scaffold types.ts, <archetype>Service.ts with an empty actions map, and the test directory mirror.
  2. Wire handle(req, res, body, fallback) into the legacy route, passing the existing switch body as fallback. Land + ship — behavior is unchanged.
  3. For each verb, in its own commit: add actions/<verb>.ts, register it in the entry point, mirror the test, delete the legacy case.
  4. When the legacy switch is empty, delete the fallback closure and drop the redundant route plumbing.

Validation

Run skill 20.build-and-lint and 21.test-all after every cutover commit. New action files must ship with their mirrored test file in the same commit.

Rollback

Each verb cutover is reversible by reverting the single commit that registered it — the legacy fallback closure remains intact during the migration, so the verb falls back to the legacy branch on revert.

Ownership

Backend team. Updates to this pattern require updating both this skill and the reference implementation in lockstep.

Anti-patterns to avoid

  • Putting business logic in <archetype>Service.ts — it is dispatch-only.
  • Adding a verb to the action registry without a mirrored test file in the same commit.
  • Importing routes/functions.ts from any file under services/<domain>/.
  • Inlining external-API fetch calls inside an action handler — go through <external>Client.ts.
  • Mutating status in an action handler without first calling assertCanAccept<Action>(currentStatus).
  • Reading credentials inside <external>Client.ts — the token is always a parameter.
  • Using console.* instead of utils/logger.
  • Forgetting to broadcast() after a status change — the frontend depends on it.

When not to use it

  • When implementing simple CRUD endpoints
  • When a route handler dispatches on fewer than ~3 branches

Prerequisites

A spec or design note listing action verbs, status enum, and external systemsThe legacy route handler exists in `routes/functions.ts` or a sibling file

Limitations

  • Not suitable for simple CRUD endpoints
  • Requires a spec or design note for action verbs and status enums
  • Requires existing legacy route handlers for incremental cutover

How it compares

This pattern provides a structured, testable approach for multi-verb domain services, unlike an unstructured `switch (action)` block in a route file.

Compared to similar skills

30-backend-service-module-pattern side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
30-backend-service-module-pattern (this skill)01moNo flagsIntermediate
architecture-patterns552moNo flagsAdvanced
nodejs-best-practices286moNo flagsAdvanced
workflow-orchestration-patterns102moNo flagsAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

architecture-patterns

wshobson

Implement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use when architecting complex backend systems or refactoring existing applications for better maintainability.

55214

nodejs-best-practices

davila7

Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying.

28120

workflow-orchestration-patterns

wshobson

Design durable workflows with Temporal for distributed systems. Covers workflow vs activity separation, saga patterns, state management, and determinism constraints. Use when building long-running processes, distributed transactions, or microservice orchestration.

10117

nestjs-expert

davila7

Nest.js framework expert specializing in module architecture, dependency injection, middleware, guards, interceptors, testing with Jest/Supertest, TypeORM/Mongoose integration, and Passport.js authentication. Use PROACTIVELY for any Nest.js application issues including architecture decisions, testing strategies, performance optimization, or debugging complex dependency injection problems. If a specialized expert is a better fit, I will recommend switching and stop.

3758

nodejs-backend-patterns

wshobson

Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.

1246

backend-patterns

affaan-m

后端架构模式、API设计、数据库优化以及针对Node.js、Express和Next.js API路由的服务器端最佳实践。

735

Search skills

Search the agent skills registry