Install
$ agentstack add skill-kennguyen887-agent-foundation-structure-a-backend-service ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
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 →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 inpackage.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.tsis 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/vsdto/responses/. Inputs validate incoming data; responses shape what goes
out (and hide internal fields).
- Every
inputs/andresponses/folder has anindex.tsbarrel 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/vsmodels/. ▸ 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 propertiescamelCase— 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 undertest/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
EntityFactorybase withmake(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.
- Author: kennguyen887
- Source: kennguyen887/agent-foundation
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.