AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Structure A Backend Service

skill-kennguyen887-agent-foundation-structure-a-backend-service · by kennguyen887

Use when scaffolding a backend service, adding a feature/module, or reviewing folder layout, file/class naming, DTO & entity structure, or the CQRS read/write split. Language-agnostic with TS/NestJS examples.

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-kennguyen887-agent-foundation-structure-a-backend-service

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kennguyen887-agent-foundation-structure-a-backend-service)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
24d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Structure A Backend Service? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

When to use

Reach for this when you are creating a new service repo, adding a feature/module to an existing one, or reviewing whether code is laid out the team way. It answers "where does this file go", "what do I name it", "how is a feature wired", "how do reads vs writes get split", and "how are tests structured".

Scope: structure, naming, and wiring — not do/don't policy (additive migrations, no process.env in business logic, HTTP-layer tests, backward-compat), which lives in the global instructions, cross-referenced inline; follow both.

Steps

Each convention: portable principle▸ Example (TS/NestJS) (neutral listing domain; ` / ` = rename) → ▸ Other stacks. Walk these in order when scaffolding.

0. Core principle (read first)

Organize by feature, not by layer. A feature owns its entry points, its read path, its write path, its DTOs, and its events. Keep the transport/entry layer thin — it only validates input and delegates. Centralize the domain model (persisted entities + domain types) and shared base classes so features stay about behavior. Drive everything from typed config, never raw env reads.

1. Lay out the repo

A service has one source tree split into feature modules, centralized domain models, two tiers of shared code, config factories, and schema migrations as a top-level sibling of source.

Example (TS/NestJS) — copy the shape, swap the domain words:

/                 # e.g. listings-service
├── src/
│   ├── main.ts                 # bootstrap: global validation pipe, filters, security headers, OpenAPI
│   ├── main.module.ts          # root module: imports every feature module + global providers
│   ├── config/                 # one factory per concern + a consts file of typed keys
│   │   ├── db.config.ts
│   │   ├── cache.config.ts
│   │   └── consts.ts           # configDb='db', configCache='cache', ... (no magic strings)
│   ├── domain/                 # domain layer: persisted models + non-persisted domain types
│   │   ├── entities/           # DB tables (ORM-mapped) — base.entity.ts + .entity.ts
│   │   └── models/             # domain enums, constants, value objects & type aliases (NO ORM/DB)
│   ├── modules/                # one folder per feature (listing, search, ...)
│   │   └── /          # see step 2 for the inside of a feature
│   ├── common/                 # app-wide shared business logic: services, constants, shared DTOs
│   └── shared/                 # lower-level: enums, utils, external-system adapters (e.g. search/)
├── libs/                       # vendored shared libraries (see "Shared libraries" below); path-alias resolved
├── migrations/                 # schema changes (DDL) — sibling of src/ (see global Migration Rules)
├── seeds/                      # initial/reference & test data (DML) — its own data-source + CLI, NOT migrations
├── test/                       # mirrors src/modules/; factories + setup live at the root (step 8)
├── ormconfig.migration.ts      # data-source used only by the migration CLI
├── .env.example                # every env var, with safe placeholder (see global Config Rules)
└── package.json
  • Two tiers of shared code, and the line between them matters. common/ = app-wide business

building blocks (shared services, shared response DTOs, domain constants). shared/ = lower-level, business-agnostic plumbing (enums, pure utils, adapters to external systems). When unsure: does it know about your domain? → common/. Could it live in any service? → shared/.

  • Truly cross-service code is a shared library, not a copied file. Base classes every service

needs (pagination response, ORM naming strategy, exception classes, auth guards, CQRS base, request logger) live in shared libraries — don't re-implement them per repo. ▸ Example (TS/NestJS): they sit under libs// (e.g. infra-common, infra-auth, infra-cqrs, infra-exception), each a package with its own src/ + build tsconfig, exposed under a placeholder path-alias scope @org/*. Two things make this work and must stay in sync:

  • Resolved by path alias, not npm install. The libs are NOT listed in package.json

dependencies; they're mapped in the compiler config (tsconfig paths) and the test runner (e.g. jest moduleNameMapper) — both pointing @org/libs//src. Add a lib → add both mappings, or build/tests break.

  • Vendored at a pinned version. libs/ is checked out from one central shared repo at a fixed

tag (e.g. a libs:build script doing a meta-repo checkout), so every service runs the same version. Bump the tag deliberately; never hand-edit vendored lib code inside a service repo. ▸ Other stacks: a published internal package (pip/Maven/npm) or a git submodule pinned to a tag — the principle is one shared, version-pinned source of truth, not copy-paste.

2. Organize a feature module

Everything one feature needs lives in its own folder; reads and writes are separate files.

Example (TS/NestJS) — inside src/modules// (here ` = listing`):

modules//
├── .module.ts        # wires this feature: imports, controllers, providers, exports
├── .controller.ts    # HTTP/REST entry  (@Controller('listings'))
├── -cmd.controller.ts# transport entry  (@MessagePattern) — co-located, same handlers
├── .service.ts       # cross-handler domain logic / external calls (only when shared)
├── commands/                  # ONE write use-case per file (create-listing.ts, publish-listing.ts)
├── queries/                   # ONE read use-case per file  (get-listing-list.ts)
├── events/                    # domain events + their handlers (*.event.ts)
├── dto/
│   ├── inputs/                # request DTOs + index.ts barrel   (see step 4)
│   └── responses/             # response DTOs + index.ts barrel  (see step 4)
└── utils/                     # feature-only helpers (*.util.ts)
  • Split the read path from the write path (CQRS). Each use-case is its own file under

commands/ (mutations) or queries/ (reads). The entry layer dispatches to a bus; it contains no business logic. This keeps each use-case independently testable and greppable. ▸ Other stacks: separate commands/ and queries/ packages of one-class-per-use-case handlers; you do not need a CQRS framework to get the benefit — the split is the point.

  • One file per use-case holds the request + its handler together. e.g. create-listing.ts

exports CreateListingCommand (the input shape) and CreateListingCommandHandler (the behavior). Don't scatter a use-case across files.

  • Co-locate multiple transports. REST and message-based entry points for the same feature sit

side by side (.controller.ts + -cmd.controller.ts) and both delegate to the same command/query handlers. Add a transport without duplicating logic.

  • A *.service.ts is for logic shared by several handlers (or external-API orchestration),

not a dumping ground. If only one handler needs it, keep it in the handler.

3. Name files and classes

Predictable names are the whole point — you should be able to guess a path.

  • Files: kebab-case. Classes: PascalCase. A file's class is the PascalCase of its name.
  • Role suffix on framework artifacts, none on use-cases.Example: *.controller.ts,

*.service.ts, *.module.ts, *.entity.ts, *.dto.ts, *.event.ts, *.config.ts, *.util.ts. Commands and queries take no suffix — the commands//queries/ folder already says what they are (create-listing.ts, not create-listing.command.ts).

  • Barrel files (index.ts) per significant folder, used for clean folder-level imports and

batch registration (providers: [...CommandHandlers, ...QueryHandlers]). ▸ Other stacks: a package __init__.py / mod.rs / package export that re-exports the folder.

  • Import paths: shared libraries via alias (e.g. @org/*); everything local via relative paths.

4. Define DTOs — split inputs from responses

DTOs are the typed contract at the boundary; requests and responses never share a folder.

  • dto/inputs/ vs dto/responses/. Inputs validate incoming data; responses shape what goes

out (and hide internal fields).

  • Every inputs/ and responses/ folder has an index.ts barrel re-exporting its DTOs, so

consumers import in one line, not one per file: import { CreateListingRequestDto, GetListingQueryDto } from '../dto/inputs';. When you add a DTO, add its export * from './x.dto'; to that folder's index.ts. ▸ Other stacks: the package's re-export file (__init__.py, mod.rs, a package index).

  • Naming encodes direction & cardinality.Example: inputs CreateRequestDto,

UpdateRequestDto, GetQueryDto; responses ResDto, ListResDto. List responses extend a shared pagination-response base.

  • Validate on input, whitelist + transform.Example: declarative validation decorators

(@IsUUID, @IsEnum, @Transform) + a global validation pipe (whitelist: true, transform: true) that rejects unknown fields and turns validation errors into the standard AppBadRequestException.

  • Control output explicitly.Example: @Exclude() on the class, @Expose() per field,

@Type(() => Nested) for nested DTOs; map entity→DTO with plainToInstance(ResDto, data) in the handler. ▸ Other stacks: an explicit serializer/schema (Pydantic model, Java DTO + mapper, Go struct with json tags) — never return the persistence model directly.

5. Define entities and access data

  • The domain layer splits in two — entities/ vs models/.Example: src/domain/entities/

holds ORM-mapped DB tables; src/domain/models/ holds domain types that are not tables — enums/value sets (Gender, Status), domain constants, and type aliases/projections (type Object = ). Pure types, no ORM decorators, no DB; one concept per kebab-case file, re-exported via the folder barrel. Put low-level/technical enums in shared/enums/ instead — domain-central value sets belong in domain/models/. ▸ Other stacks: a domain/ package split into persisted models and plain enums/value-objects/types.

  • All persistence models in one place, on a shared base.Example: src/domain/entities/

(not inside features). Every entity extends BaseEntity, which adds createdAt/updatedAt/isDeleted (audit + soft delete) — these are select: false, so you opt in. Soft-deleted rows are excluded with an explicit WHERE isDeleted = false. ▸ Other stacks: a base model / mixin contributing the same audit + soft-delete columns.

  • DB columns snake_case, code properties camelCase — bridged by a naming strategy, so you

never hand-name columns. ▸ Other stacks: the ORM's snake-case-to-camel mapping config.

  • Use the ORM's repository directly + a query builder; no custom repository wrapper layer.

Example: inject the typed repository into the handler and build queries with the query builder; select only the columns you need. Push every row filter (status/type/date/soft-delete) into the query — do not fetch broadly and filter in code (this is also a global rule).

  • Money/decimals go through a decimal transformer (fixed precision/scale + a decimal type),

never raw floats.

  • Migrations vs seeds are different tools — keep them apart. Migrations change the schema

(DDL: create/alter/drop tables & columns) or transform existing data; they are reversible (up/down) and immutable once merged. Seeds insert initial/reference or test data (DML). ▸ Example: migrations/ with its own data-source + CLI, and a separate seeds/ with its own data-source + CLI. Never seed production data inside a migration. For the full migration rules (additive-only, reversible, run on a fresh DB before commit) follow the global Database & Migration Rules — not restated here.

6. Wire the code patterns

  • Thin entry layer. Controllers validate the DTO and dispatch to the matching handler; no

business logic. (Matches the global "request handlers read top-to-bottom in one screenful" rule.)

  • Errors: throw typed domain exceptions, translate once at the edge.Example: handlers

throw AppBadRequestException / AppNotFoundException; a single global exception filter maps them to the HTTP/transport response. Don't format error responses inside handlers. ▸ Other stacks: a custom exception hierarchy + one global error handler/middleware.

  • Side effects via domain events, not inline calls.Example: a handler emits an event

(StatusChangedEvent); an event handler publishes outward (an external pub/sub). Keep the use-case's own logic free of fan-out. ▸ Other stacks: an event bus / outbox.

  • External systems behind an adapter + base service in shared/. ▸ Example:

shared//-base.service.ts centralizes auth/URL/error handling; adapters translate domain↔external shapes. Business code calls the adapter, never the HTTP client directly.

  • Cross-cutting concerns are global, declared once (request logging, activity log, security

headers, auth guards) — not re-added per controller.

7. Configuration

  • No raw env in business code.Example: one config factory per concern in src/config/

(registerAs('', () => ({...}))), keys are constants in a config/consts file, read via configService.get(configDb). ▸ Other stacks: a typed settings object / config service.

  • Per-environment files (.env.), and **every var in .env.example + validated at

boot (global Config & Environment Rules** — fail fast, no silent fallback).

8. Testing (unit & integration)

The team tests through the outermost boundary, not isolated internals — so a "unit" of behavior is verified the way it actually runs. Boot the app once, build data with factories, hit the real transport, assert response and persisted state.

  • Tests mirror src/modules/. One spec per controller/use-case under test/modules//;

factories and the boot harness live at the test root.

  • Boot the app once, share a context. A single setup file builds the app from the **root

module, overrides only external infra** (job queue, logger) with mocks, starts the transport

  • a client, and exposes a global testContext = { app, module, client, dataSource }. An

aggregator entry globs and requires every *.spec.ts so the whole suite boots once (fast, shared DB). ▸ Example: test/setup-app.ts + a test/.e2e.ts entry. ▸ Other stacks: a shared fixture / conftest that starts the app + a real test DB once per run.

  • Exercise via the real boundary. Each test sends a real request through the transport/HTTP

client and asserts both the response shape and the persisted DB row (re-read it from the repository). Re-apply the same global validation pipe the app uses in prod.

  • Mock only what crosses the process boundary. Stub external service calls

(jest.spyOn(ExternalService.prototype, 'method')) and outbound HTTP (an HTTP-intercept lib); use a real database. Never mock the unit under test. Assert outbound side effects with spy-called-with.

  • Factories build data; clean up per test. An EntityFactory base with make(overrides)

(random values via a faker lib) and build() (persist), plus a FactoryContext(dataSource) that tracks created rows and tears them down in afterEach. ▸ Other stacks: factory_boy / test-data builders + per-test truncation or a transactional rollback.

  • No isolated unit tests that bypass the boundary. This matches the global **HTTP-layer testing

rule**: if a behavior can't be reached through the boundary, skip it rather than unit-testing internals (no tests/services/**, tests/validators/**, tests/utils/** that byp

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.