Review — Backend (NestJS / API / DB)
Focused backend reviewer covering NestJS patterns, REST API design, database access patterns,
and TypeScript correctness for service-layer code. This skill does NOT duplicate security or
performance checks — those belong to review-security-code and review-performance.
Workflow
review-backend Progress:
- [ ] Step 1: Read Job Context (if CONTEXT_PATH provided)
- [ ] Step 2: Determine git scope (merge-base) — see skills/shared/git-merge-base.md
- [ ] Step 3: Collect diff and changed file list
- [ ] Step 4: NestJS patterns check
- [ ] Step 5: API design check
- [ ] Step 6: Database patterns check
- [ ] Step 7: TypeScript correctness check
- [ ] Step 8: Emit findings in unified format, sorted by severity
Input Contract
| Field |
Type |
Required |
Description |
branch |
string |
no |
Branch to review. Defaults to current branch. |
commit_range |
string |
no |
Explicit range (e.g., abc123..HEAD). Overrides merge-base detection. |
context_doc |
string |
no |
Path to job context document (e.g., <JOBS_ROOT>/<job>/ai/context.md). |
issue_url |
string |
no |
GitHub issue or task URL for spec compliance reference. |
Scope Detection
See shared script: skills/shared/git-merge-base.md
Run that script to determine BASE_SHA before collecting the diff.
# Default mode — all changes from merge-base to working tree
git diff --name-only "${BASE_SHA}"
git diff "${BASE_SHA}"
# Explicit range mode
git diff --name-only <FROM_SHA>..<TO_SHA>
git diff <FROM_SHA>..<TO_SHA>
Only review files changed in scope. Do not comment on legacy code outside the diff.
Review Checklist
1. NestJS Patterns
Module structure
@Module() declaration present; providers, imports, exports are correctly listed
- Circular dependency risk: two modules importing each other — flag as
major if detected; suggest forwardRef()
- Feature modules do not import
AppModule
Provider scope
- Default scope is
Singleton — correct for stateless services
REQUEST-scoped provider injected into a Singleton provider is a blocker (creates hidden state sharing)
TRANSIENT scope used unnecessarily when singleton would suffice — flag as minor
DTO validation
- All controller methods accepting user input (body, query, param) have a DTO class decorated with
class-validator decorators
- Required fields:
@IsString(), @IsNumber(), @IsEmail(), etc. present and accurate
@IsOptional() used only on truly optional fields; non-optional fields must not have @IsOptional()
ValidationPipe applied globally (in main.ts) or explicitly via @UsePipes(ValidationPipe) — blocker if missing on any endpoint accepting user input
@Transform() used where input coercion is needed (e.g., string → number from query params)
Guards and roles
- Protected endpoints have
@UseGuards(...) with appropriate guard(s)
- Role-based access uses
@Roles(...) decorator with a roles guard — missing guard on an endpoint that should be protected is a blocker
- Public endpoints explicitly marked with
@Public() or equivalent decorator, not just left unguarded
Controller responsibilities
- Controllers are thin: no business logic, no direct ORM calls, no computation
- Controller methods do: parse input → delegate to service → return response
- If business logic is in a controller method, flag as
major ("move to service layer")
Service layer
- Business logic lives in services
- Services do not directly call the ORM in complex multi-step ways — that belongs in a repository or query object
- Services do not import
Request/Response from HTTP framework (except for streaming or special cases)
- Services declare return types on all public methods
2. API Design
HTTP method conventions
GET requests carry no body (flag as major if present)
POST used for resource creation
PUT for full replacement, PATCH for partial update — mixed usage without clear reason flagged as minor
DELETE on correct resource path
Response shape consistency
- All endpoints in a controller return the same envelope shape (e.g.,
{ data, meta }) — inconsistency (sometimes object, sometimes raw array) flagged as major
- HTTP status codes match semantics:
201 for create, 200 for read/update, 204 for no-content delete, 404 for not found, 422 for validation failure
Pagination
- List endpoints that could return unbounded datasets must have pagination (
limit/offset or cursor)
- Missing pagination on a list endpoint flagged as
major when no obvious bound exists
Error handling
- Async controller methods must have error handling: either
try/catch or a global exception filter
- Unhandled promise rejections (async method without try/catch, no global filter) — blocker
- Exception messages must not leak internal details (stack traces, ORM error strings) to the HTTP response — leaking is a blocker
- NestJS built-in exceptions (
NotFoundException, BadRequestException, etc.) preferred over generic Error
3. Database Patterns (ORM)
N+1 queries
- Relation loaded inside a loop without eager loading (
relations, leftJoinAndSelect) or a DataLoader — always major
- Example:
for (const user of users) { user.orders = await orderRepo.find(...) } — flag with concrete suggestion to use relations: ['orders'] or batch query
Transactions
- Multi-step operations that must be atomic (e.g., create + update + delete across tables) must use a transaction
- Missing transaction on a multi-step mutation is a major finding
- NestJS TypeORM: use
dataSource.transaction(async (manager) => { ... }) or @Transaction() decorator pattern
Raw query safety
- String interpolation in raw queries is a blocker (SQL injection risk)
- Use parameterized queries:
query('SELECT * FROM users WHERE id = $1', [id])
- Flag
createQueryBuilder().where('id = ' + id) as blocker
Soft deletes
- If the project schema uses
deletedAt / @DeleteDateColumn(), deletions must use .softRemove() / .softDelete(), not .remove() / .delete()
- Hard delete on a soft-delete entity flagged as
major
Migration safety
- Column removed in migration — check that no application code still references it
- Dropping a column without a deprecation period in a live system flagged as
major
NOT NULL column added without a default value to a populated table — blocker
4. TypeScript Correctness
- No
any in new code — suggest unknown with type guards, or correct typed return
- Public service methods must have explicit return types (
Promise<UserDto>, not inferred)
- Service contracts expressed as interfaces (
IUserService), not just class implementations
- No
as any or unsafe casts (as unknown as T) without a comment explaining why
@ts-ignore / @ts-expect-error without explanation comment — flag as minor
Iron Laws
| Condition |
Severity |
| Missing DTO validation on an endpoint accepting user input |
blocker |
REQUEST-scoped provider injected into Singleton |
blocker |
| Leaking stack trace / raw ORM error to API response |
blocker |
| Unhandled promise rejection in controller method |
blocker |
| String interpolation in raw SQL query |
blocker |
| N+1 query (relation loaded in a loop) |
major (minimum) |
| Missing transaction on multi-step atomic mutation |
major |
| Business logic in controller |
major |
| Missing pagination on unbounded list endpoint |
major |
NOT NULL column added without default to populated table (migration) |
blocker |
Orchestrated Review Contract
When dispatched by review-orchestrator, follow the provided reviewer-input.schema.json payload. Return a REVIEW_RESULT object compatible with skills/review-orchestrator/reviewer-finding.schema.json, then a concise markdown summary. Keep findings evidence-based, include concrete suggested_fix for every blocker/major, and return NEEDS_CONTEXT instead of guessing when required context is missing.
Finding Format
### [F-001] Title
- **Severity**: blocker | major | minor | info
- **File**: path/to/file.ts:line
- **Problem**: what is wrong
- **Why it matters**: impact on correctness / safety / maintainability
- **Fix**: concrete suggestion
- **Patch** (optional):
```diff
- old line
+ new line
---
## Output Contract
STATUS: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
- `DONE` — no blockers or majors found
- `DONE_WITH_CONCERNS` — one or more blocker or major findings present
- `NEEDS_CONTEXT` — cannot determine intent without context doc or issue; state what is missing
- `BLOCKED` — cannot access diff or required files; state reason
```markdown
# Backend Review Report
## Verdict: APPROVE | APPROVE_WITH_SUGGESTIONS | REQUEST_CHANGES
## Summary
<2-4 sentences covering what changed, overall backend health, key concerns.>
## Review Scope
- Branch: `<BRANCH>`
- Parent ref: `<PARENT>`
- Merge-base: `<BASE_SHA>`
- Scope mode: `<default-with-uncommitted | explicit-hash-range>`
- Changed files: <count>
## Stats
- blocker: N
- major: N
- minor: N
- info: N
## Blockers (must fix before merge)
<[F-NNN] findings>
## Major Issues
<[F-NNN] findings>
## Minor & Info
<[F-NNN] findings>
## Positive Notes
<Optional. Things done well.>
Scope Boundaries
| Concern |
This skill |
Use instead |
| NestJS module / provider / DTO / controller / service |
YES |
— |
| REST API shape, HTTP methods, error handling |
YES |
— |
| Database ORM patterns, N+1, transactions, migrations |
YES |
— |
| TypeScript strictness in service layer |
YES |
— |
| XSS, injection (beyond SQL in raw queries), auth bypass |
NO |
review-security-code |
| Bundle size, query latency profiling |
NO |
review-performance |
| Frontend, React, MobX |
NO |
review-frontend |
| Architectural layer violations (cross-module) |
NO |
review-architecture |
| Naming, style, import order |
NO |
review-style |
Job Context Awareness
When dispatched by job-orchestrator or called with an explicit context path, the prompt MAY include:
JOB_NAME: <job-name>
CONTEXT_PATH: <JOBS_ROOT>/<job-name>/ai/context.md
If provided and the file exists, read the context document before running scope detection.
Use it to understand:
- Which ORM, validation library, or guard strategy was intentionally chosen
- Project-level architectural decisions (e.g., no repository pattern by design)
- Acceptance criteria to verify spec compliance
If absent, proceed normally — context is optional and non-blocking.
Red Flags
| Rationalization |
Why it is wrong |
| "ValidationPipe is probably configured somewhere" |
Always verify it is applied; assumption lets injection reach service layer |
| "The N+1 only runs on small datasets now" |
Data grows; flag it now with a clear fix, not after the incident |
| "The controller has some logic but it's minor" |
The line is binary — logic in controller = untestable without HTTP stack |
| "Stack trace in response only shows in dev" |
Config can be wrong in prod; treat it as blocker always |
| "I'll skip the migration check — it's just a column rename" |
Column renames without fallback break zero-downtime deploys |
1---2name: review-backend3description: Use when: reviewing NestJS backend changes — API design, service layer, DTO validation, database patterns, and TypeScript correctness. Covers "review backend", "backend review", "review API", "review NestJS", or dispatched by review-orchestrator with --backend flag. NOT for: frontend patterns, MobX, React components, general security vulnerabilities (use review-security-code for XSS/injection/auth-bypass), or performance profiling (use review-performance).4license: MIT5---67# Review — Backend (NestJS / API / DB)89Focused backend reviewer covering NestJS patterns, REST API design, database access patterns,10and TypeScript correctness for service-layer code. This skill does NOT duplicate security or11performance checks — those belong to `review-security-code` and `review-performance`.1213---1415## Workflow1617```18review-backend Progress:19- [ ] Step 1: Read Job Context (if CONTEXT_PATH provided)20- [ ] Step 2: Determine git scope (merge-base) — see skills/shared/git-merge-base.md21- [ ] Step 3: Collect diff and changed file list22- [ ] Step 4: NestJS patterns check23- [ ] Step 5: API design check24- [ ] Step 6: Database patterns check25- [ ] Step 7: TypeScript correctness check26- [ ] Step 8: Emit findings in unified format, sorted by severity27```2829---3031## Input Contract3233| Field | Type | Required | Description |34|-------|------|----------|-------------|35| `branch` | string | no | Branch to review. Defaults to current branch. |36| `commit_range` | string | no | Explicit range (e.g., `abc123..HEAD`). Overrides merge-base detection. |37| `context_doc` | string | no | Path to job context document (e.g., `<JOBS_ROOT>/<job>/ai/context.md`). |38| `issue_url` | string | no | GitHub issue or task URL for spec compliance reference. |3940---4142## Scope Detection4344See shared script: `skills/shared/git-merge-base.md`4546Run that script to determine `BASE_SHA` before collecting the diff.4748```bash49# Default mode — all changes from merge-base to working tree50git diff --name-only "${BASE_SHA}"51git diff "${BASE_SHA}"5253# Explicit range mode54git diff --name-only <FROM_SHA>..<TO_SHA>55git diff <FROM_SHA>..<TO_SHA>56```5758Only review files changed in scope. Do not comment on legacy code outside the diff.5960---6162## Review Checklist6364### 1. NestJS Patterns6566**Module structure**67- `@Module()` declaration present; providers, imports, exports are correctly listed68- Circular dependency risk: two modules importing each other — flag as `major` if detected; suggest `forwardRef()`69- Feature modules do not import `AppModule`7071**Provider scope**72- Default scope is `Singleton` — correct for stateless services73- `REQUEST`-scoped provider injected into a `Singleton` provider is a **blocker** (creates hidden state sharing)74- `TRANSIENT` scope used unnecessarily when singleton would suffice — flag as `minor`7576**DTO validation**77- All controller methods accepting user input (body, query, param) have a DTO class decorated with `class-validator` decorators78- Required fields: `@IsString()`, `@IsNumber()`, `@IsEmail()`, etc. present and accurate79- `@IsOptional()` used only on truly optional fields; non-optional fields must not have `@IsOptional()`80- `ValidationPipe` applied globally (in `main.ts`) or explicitly via `@UsePipes(ValidationPipe)` — **blocker** if missing on any endpoint accepting user input81- `@Transform()` used where input coercion is needed (e.g., string → number from query params)8283**Guards and roles**84- Protected endpoints have `@UseGuards(...)` with appropriate guard(s)85- Role-based access uses `@Roles(...)` decorator with a roles guard — missing guard on an endpoint that should be protected is a **blocker**86- Public endpoints explicitly marked with `@Public()` or equivalent decorator, not just left unguarded8788**Controller responsibilities**89- Controllers are thin: no business logic, no direct ORM calls, no computation90- Controller methods do: parse input → delegate to service → return response91- If business logic is in a controller method, flag as `major` ("move to service layer")9293**Service layer**94- Business logic lives in services95- Services do not directly call the ORM in complex multi-step ways — that belongs in a repository or query object96- Services do not import `Request`/`Response` from HTTP framework (except for streaming or special cases)97- Services declare return types on all public methods9899---100101### 2. API Design102103**HTTP method conventions**104- `GET` requests carry no body (flag as `major` if present)105- `POST` used for resource creation106- `PUT` for full replacement, `PATCH` for partial update — mixed usage without clear reason flagged as `minor`107- `DELETE` on correct resource path108109**Response shape consistency**110- All endpoints in a controller return the same envelope shape (e.g., `{ data, meta }`) — inconsistency (sometimes object, sometimes raw array) flagged as `major`111- HTTP status codes match semantics: `201` for create, `200` for read/update, `204` for no-content delete, `404` for not found, `422` for validation failure112113**Pagination**114- List endpoints that could return unbounded datasets must have pagination (`limit`/`offset` or `cursor`)115- Missing pagination on a list endpoint flagged as `major` when no obvious bound exists116117**Error handling**118- Async controller methods must have error handling: either `try/catch` or a global exception filter119- Unhandled promise rejections (async method without try/catch, no global filter) — **blocker**120- Exception messages must not leak internal details (stack traces, ORM error strings) to the HTTP response — leaking is a **blocker**121- NestJS built-in exceptions (`NotFoundException`, `BadRequestException`, etc.) preferred over generic `Error`122123---124125### 3. Database Patterns (ORM)126127**N+1 queries**128- Relation loaded inside a loop without eager loading (`relations`, `leftJoinAndSelect`) or a DataLoader — **always major**129- Example: `for (const user of users) { user.orders = await orderRepo.find(...) }` — flag with concrete suggestion to use `relations: ['orders']` or batch query130131**Transactions**132- Multi-step operations that must be atomic (e.g., create + update + delete across tables) must use a transaction133- Missing transaction on a multi-step mutation is a **major** finding134- NestJS TypeORM: use `dataSource.transaction(async (manager) => { ... })` or `@Transaction()` decorator pattern135136**Raw query safety**137- String interpolation in raw queries is a **blocker** (SQL injection risk)138- Use parameterized queries: `query('SELECT * FROM users WHERE id = $1', [id])`139- Flag `createQueryBuilder().where('id = ' + id)` as blocker140141**Soft deletes**142- If the project schema uses `deletedAt` / `@DeleteDateColumn()`, deletions must use `.softRemove()` / `.softDelete()`, not `.remove()` / `.delete()`143- Hard delete on a soft-delete entity flagged as `major`144145**Migration safety**146- Column removed in migration — check that no application code still references it147- Dropping a column without a deprecation period in a live system flagged as `major`148- `NOT NULL` column added without a default value to a populated table — **blocker**149150---151152### 4. TypeScript Correctness153154- No `any` in new code — suggest `unknown` with type guards, or correct typed return155- Public service methods must have explicit return types (`Promise<UserDto>`, not inferred)156- Service contracts expressed as interfaces (`IUserService`), not just class implementations157- No `as any` or unsafe casts (`as unknown as T`) without a comment explaining why158- `@ts-ignore` / `@ts-expect-error` without explanation comment — flag as `minor`159160---161162## Iron Laws163164| Condition | Severity |165|-----------|----------|166| Missing DTO validation on an endpoint accepting user input | **blocker** |167| `REQUEST`-scoped provider injected into `Singleton` | **blocker** |168| Leaking stack trace / raw ORM error to API response | **blocker** |169| Unhandled promise rejection in controller method | **blocker** |170| String interpolation in raw SQL query | **blocker** |171| N+1 query (relation loaded in a loop) | **major** (minimum) |172| Missing transaction on multi-step atomic mutation | **major** |173| Business logic in controller | **major** |174| Missing pagination on unbounded list endpoint | **major** |175| `NOT NULL` column added without default to populated table (migration) | **blocker** |176177---178179## Orchestrated Review Contract180181When dispatched by `review-orchestrator`, follow the provided `reviewer-input.schema.json` payload. Return a `REVIEW_RESULT` object compatible with `skills/review-orchestrator/reviewer-finding.schema.json`, then a concise markdown summary. Keep findings evidence-based, include concrete `suggested_fix` for every blocker/major, and return `NEEDS_CONTEXT` instead of guessing when required context is missing.182183---184185## Finding Format186187```markdown188### [F-001] Title189190- **Severity**: blocker | major | minor | info191- **File**: path/to/file.ts:line192- **Problem**: what is wrong193- **Why it matters**: impact on correctness / safety / maintainability194- **Fix**: concrete suggestion195- **Patch** (optional):196 ```diff197 - old line198 + new line199 ```200```201202---203204## Output Contract205206```207STATUS: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED208```209210- `DONE` — no blockers or majors found211- `DONE_WITH_CONCERNS` — one or more blocker or major findings present212- `NEEDS_CONTEXT` — cannot determine intent without context doc or issue; state what is missing213- `BLOCKED` — cannot access diff or required files; state reason214215```markdown216# Backend Review Report217218## Verdict: APPROVE | APPROVE_WITH_SUGGESTIONS | REQUEST_CHANGES219220## Summary221<2-4 sentences covering what changed, overall backend health, key concerns.>222223## Review Scope224- Branch: `<BRANCH>`225- Parent ref: `<PARENT>`226- Merge-base: `<BASE_SHA>`227- Scope mode: `<default-with-uncommitted | explicit-hash-range>`228- Changed files: <count>229230## Stats231- blocker: N232- major: N233- minor: N234- info: N235236## Blockers (must fix before merge)237<[F-NNN] findings>238239## Major Issues240<[F-NNN] findings>241242## Minor & Info243<[F-NNN] findings>244245## Positive Notes246<Optional. Things done well.>247```248249---250251## Scope Boundaries252253| Concern | This skill | Use instead |254|---------|------------|-------------|255| NestJS module / provider / DTO / controller / service | YES | — |256| REST API shape, HTTP methods, error handling | YES | — |257| Database ORM patterns, N+1, transactions, migrations | YES | — |258| TypeScript strictness in service layer | YES | — |259| XSS, injection (beyond SQL in raw queries), auth bypass | NO | `review-security-code` |260| Bundle size, query latency profiling | NO | `review-performance` |261| Frontend, React, MobX | NO | `review-frontend` |262| Architectural layer violations (cross-module) | NO | `review-architecture` |263| Naming, style, import order | NO | `review-style` |264265---266267## Job Context Awareness268269When dispatched by `job-orchestrator` or called with an explicit context path, the prompt MAY include:270271```272JOB_NAME: <job-name>273CONTEXT_PATH: <JOBS_ROOT>/<job-name>/ai/context.md274```275276If provided and the file exists, read the context document **before** running scope detection.277Use it to understand:278- Which ORM, validation library, or guard strategy was intentionally chosen279- Project-level architectural decisions (e.g., no repository pattern by design)280- Acceptance criteria to verify spec compliance281282If absent, proceed normally — context is optional and non-blocking.283284---285286## Red Flags287288| Rationalization | Why it is wrong |289|----------------|-----------------|290| "ValidationPipe is probably configured somewhere" | Always verify it is applied; assumption lets injection reach service layer |291| "The N+1 only runs on small datasets now" | Data grows; flag it now with a clear fix, not after the incident |292| "The controller has some logic but it's minor" | The line is binary — logic in controller = untestable without HTTP stack |293| "Stack trace in response only shows in dev" | Config can be wrong in prod; treat it as blocker always |294| "I'll skip the migration check — it's just a column rename" | Column renames without fallback break zero-downtime deploys |