AI Unified Process service and API layers - NestJS services, REST controllers, and their tests
94%
Does it follow best practices?
Impact
100%
1.11x2 of 3 eval scenarios. Add 1 more for a full score.
Low
Low-risk findings worth noting
A lookup used by /nest-service, /nest-rest, their test skills, and the Drizzle skills before writing any code. It
answers one question: where does the API application live, and which of its conventions must new code match? The
## Module layout section of docs/architecture/stack.md wins where it records an answer; detect the rest.
Never assume. Get ESM/NodeNext wrong and nothing compiles: a NodeNext project requires a .js suffix on every
relative import even though the source file is .ts. Omit it and the build fails; add it in a project that is not
NodeNext and the build fails the other way.
| # | Question | Signal | Consequence if wrong |
|---|---|---|---|
| 1 | API app root | The workspace whose package.json has @nestjs/core in dependencies; without workspaces, the folder of nest-cli.json | Code lands in the wrong app |
| 2 | ESM/NodeNext | "type": "module" in the API's package.json and "module": "NodeNext" (or "Node16") in its tsconfig.json | Missing .js suffixes; nothing compiles |
| 3 | Shared contract package | A workspace package imported by the API and a client that exports request and response types | Duplicated, drifting types |
Read the repo-root package.json and look for workspaces. Expand each glob and read every matched package.json.
node -e "console.log(require('./package.json').workspaces)"A monorepo commonly puts the application at apps/api, but the name is arbitrary: resolve it from the dependency, not
from the directory name. State the root you found before writing anything, so a wrong guess is visible at once.
node -e "const p=require('./<api>/package.json'); console.log(p.type)"
grep -E '"module"|"moduleResolution"' <api>/tsconfig.json"type": "module" together with "module": "NodeNext" means every relative import specifier ends in .js:
import { ProductsService } from './products.service.js'; // correct: the source is .ts
import { ProductsService } from './products.service'; // fails to resolveThe quickest confirmation is the project's own code: open any file with a relative import and copy what it does. Test files follow the same rule.
Every feature is a folder under src/<feature>/ with the same anatomy:
<feature>.module.ts declares the controller and providers
<feature>.controller.ts /nest-rest: routing, DTO binding; no business logic
<feature>.service.ts /nest-service: business rules, orchestration, domain errors
<feature>.repository.ts the persistence skill: all database access [if the feature owns data]
dto/*.ts /nest-rest: class-validator request DTOs and response typesRegister the module in the application's root module: a feature that compiles but is never imported produces a 404
that looks like a routing bug. Whichever layer creates the folder first creates <feature>.module.ts; the layers above
add their provider or controller to it.
Find one implemented feature and copy its exact shape. The table says where things live; an existing feature says how this team writes them.
*.repository.ts, or do features consume shared
repositories exported by a core module? Match whichever exists.dto/, or imported from a shared contract package? With a shared
package, a new shape belongs there too, even when the package does not mention this use case yet.Fall back to these defaults rather than inventing a structure, and say which ones you applied, so the first feature's conventions are a visible decision:
*.repository.ts inside the feature folder.dto/ folder.src/common/errors.ts with the domain errors, mapped by one exception filter (/nest-rest).ValidationPipe and the exception filter registered with APP_PIPE and APP_FILTER in the root module rather than
in main.ts, so a test application built from the root module has them too.<Feature>Controller routing, DTO binding, mapping to the response type; no logic
→ <Feature>Service business rules; throws domain errors
→ <Feature>Repository every query lives here
→ schema the tables
← <Feature>Response mapped shape, never a raw rowNever let a query escape the repository, and never let a raw database row reach the controller's return type.