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.zipInstalls 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.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
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.tsor a sibling file. The cutover is incremental — the registry'sfallbackkeeps 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
- 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. - 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.,delete→destroy). Apply at the boundary so the frontend wire format is unchanged.handle(req, res, body, fallback)resolves the verb, dispatches, or invokesfallback()for verbs not yet migrated.- Export
register…Action,_reset…ForTests,_getRegistered…(underscore = test-only).
- Handler signature —
export async function <verb>Action(ctx: <Archetype>Context): Promise<void>. Handlers respond directly viactx.resand returnvoid; they never throw to the registry for expected error paths. - Context shape — defined once in
types.ts:export interface <Archetype>Context { req: Request; res: Response; body: Record<string, unknown> & { action: <ActionVerb>; <primaryId>: string; … }; } - Status machine —
types.tsexports the status string-literal union plusTRANSITIONAL/TERMINALReadonlySets andisTransitional/isTerminalpredicates.statusMachine.tsexportsassertCanAccept<Action>(currentStatus)returning hints (e.g.,{ clearFailureAttrs: boolean }) and a customErrorsubclass carrying the offending status. See statusMachine.ts. - Pure helpers —
naming.tsand similar pure modules take a plain options object, return a plain result object, and contain zero I/O. They are pinned by exhaustiveit.each([...])tables innaming.test.ts. - External client — one
<external>Client.tsper external system (e.g.,genappWorkflowClient.ts). Takes the auth token as a parameter; never reads credentials itself. Exports narrow…Paramsinterfaces alongside each function. Module-levelconstreads env vars with sensible defaults documented in a comment. - Shared failure helpers — extract repeated
UPDATE … status='failed' … + broadcastblocks intoactions/_failure.ts. See actions/_failure.ts forrecordFailureandformatDispatchHttpCause. - Logging — every module declares
const LOG_PREFIX = "[<domain>-<archetype>:<verb>]"and logs throughutils/logger, neverconsole. - Broadcasts — DB status changes are followed by
broadcast(\<channel>-${projectId}`, "<event>", payload)fromutils/websocket` so the frontend receives push updates. - 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. Exportstick…Poller()for deterministic test ticks plusstart…Poller()/stop…Poller()forindex.ts. See poller.ts. - No coupling to the legacy file — the new module MUST NOT import from
routes/functions.ts. The only bridge is the runtimefallbackclosure passed tohandle().
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 }whereres.statusandres.jsonare chainablejest.fn().mockReturnThis(). - One
describeper handler;ittitles 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
- Scaffold
types.ts,<archetype>Service.tswith an emptyactionsmap, and the test directory mirror. - Wire
handle(req, res, body, fallback)into the legacy route, passing the existingswitchbody asfallback. Land + ship — behavior is unchanged. - For each verb, in its own commit: add
actions/<verb>.ts, register it in the entry point, mirror the test, delete the legacycase. - When the legacy
switchis 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.tsfrom any file underservices/<domain>/. - Inlining external-API
fetchcalls 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 ofutils/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
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.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| 30-backend-service-module-pattern (this skill) | 0 | 1mo | No flags | Intermediate |
| architecture-patterns | 55 | 2mo | No flags | Advanced |
| nodejs-best-practices | 28 | 6mo | No flags | Advanced |
| workflow-orchestration-patterns | 10 | 2mo | No flags | Advanced |
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.
nodejs-best-practices
davila7
Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying.
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.
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.
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.
backend-patterns
affaan-m
后端架构模式、API设计、数据库优化以及针对Node.js、Express和Next.js API路由的服务器端最佳实践。