AI coding agents are good at writing NestJS code and bad at guessing your NestJS conventions. Left alone, they put queries in controllers, return entities straight from the database, add a fourth way of throwing errors, and cheerfully offer to run migration:run against whatever database your .env points at.
The fix is not a cleverer prompt. It's a repository that states its rules where agents read them, makes correct changes cheap to verify, and enforces the few rules that must never be broken. Here's the structure I use. None of it needs a particular agent.
Every major agent now reads a project instruction file. Codex, Cursor, GitHub Copilot and Gemini CLI (with one setting) read AGENTS.md. Claude Code reads CLAUDE.md, which can import another file. So:
<!-- CLAUDE.md -->
@AGENTS.md
## Claude Code specifics
(skills, subagents, permission rules: only what is genuinely Claude-specific)
// .gemini/settings.json
{ "context": { "fileName": ["AGENTS.md"] } }
Rules live in AGENTS.md only. The day you have the same rule in two files, they start to drift, and some tools (Copilot CLI, for one) merge every instruction file they find without a precedence order. Keep AGENTS.md short (a project map, commands, conventions, safety rules, report format) and link to docs/ for detail.
"Write clean code" changes nothing. What changes behaviour is a short list of things the agent must not do, written as rules with the exact words you want back:
## Stop rules
1. Do not generate, run or revert migrations until the user tells you to in this conversation.
An entity change is not permission. Make the code change and report:
"Schema change detected. Migration required. Not generated or run: no explicit approval."
2. Do not run or edit seeds until the user tells you to.
3. Do not format code. The pre-commit hook formats staged files; CI checks formatting.
Rule 3 is a cost rule: formatting runs burn agent time and produce noisy diffs, and Husky + lint-staged already do it on every commit.
Agents copy what they see. If the codebase has one pattern, they copy that one. Pick an explicit structure and write it down:
src/<feature>/
controllers/ HTTP only: routing, guards, Swagger decorators
services/ business rules; one class per responsibility
dto/ request DTOs (class-validator, length limits) and response DTOs
entities/ the data model; no queries, HTTP, email or JWT logic
Then the dependency rule in one line: Controller → Service → Repository<Entity>. Other modules inject the owning service, never another module's repository. And a few "never" rules that agents otherwise break constantly:
ResponseDto.fromEntity(); never return an entity (that's how password hashes end up in JSON).@CurrentUser(), never by an ID from the request alone.
A global ValidationPipe with whitelist: true, forbidNonWhitelisted: true backs the last rule up mechanically: a DTO that doesn't declare status can't receive it.
Give the agent exactly one way to fail:
export abstract class AppException extends Error { /* status + stable code */ }
export class NotFoundError extends AppException {}
export class ConflictError extends AppException {}
// ...
throw new ConflictError('EMAIL_ALREADY_REGISTERED', 'Email is already registered');
A single global exception filter turns every error, including unexpected ones, into the same shape:
{ "statusCode": 409, "code": "EMAIL_ALREADY_REGISTERED", "message": "...",
"path": "/api/v1/auth/register", "requestId": "6f1c...", "timestamp": "..." }
Stable UPPER_SNAKE codes give agents (and frontends) something to assert on, and the filter is the one place that guarantees no SQL, stack trace or SMTP detail leaks.
An agent will run the checks you make easy. If tests need a database, Docker and seed data, the agent either skips them or starts containers you didn't ask for. So make the default suites infrastructure-free:
/** Minimal in-memory stand-in for the TypeORM repository methods used by the services. */
export class MockRepository<T extends { id: string }> {
readonly items = new Map<string, T>();
create(data: Partial<T> = {}): T { /* ... */ }
async save(entity: Partial<T>): Promise<T> { /* ... */ }
// findOne, findAndCount, update... only what's used
}
AppModule with the real pipes, guards, filters and throttler, and override only the repositories and the mail service. That tests authorization and validation where the bugs actually are, in about ten seconds, with no database.
Then tell the agent the exact commands, narrowest first (npx jest <path>), and that a red test is fixed for a real reason, never by weakening the assertion.
Be honest about the gap: in-memory repositories don't test SQL, constraints or cascades. Say so in docs/testing.md, and check migrations by hand (up, down, up) against a disposable PostgreSQL before a release.
Written rules are followed most of the time. For the few operations that can destroy data, add enforcement your tool supports. In Claude Code, ask permission rules prompt before matching commands even in permissive modes:
{
"permissions": {
"ask": ["Bash(npm run migration:*)", "Bash(npm run seed *)", "Bash(psql *)",
"Bash(docker compose down *)", "Bash(git push *)"],
"deny": ["Read(**/.env)", "Read(**/.env.production)"]
}
}
Know the limits: these rules match command text. A script that runs a migration indirectly isn't caught, and a deny on reading .env doesn't stop code that opens the file. For other agents, configure their command approval for the same commands.
The most useful line in my AGENTS.md is the report format:
Implemented what changed and why
Files changed paths
Verification commands actually run and their actual results
Not performed e.g. migration generation, seed execution, Docker, deploy
Unverified anything you could not check
Never claim a command passed if you did not run it.
It turns "Done! Everything works" into something you can check in thirty seconds, and the "Not performed" line is where the pending migration shows up instead of silently happening.
When you notice you're giving the same multi-step instruction for the third time, make it a file. In Claude Code these are skills (.claude/skills/<name>/SKILL.md), loaded on demand; for other agents the same Markdown works as "follow .claude/skills/debug/SKILL.md". Good candidates in a NestJS API:
down(), and never generate one without a request. 400 property X should not exist → the DTO is missing a field), reproduce with a database-free test, fix at the root.
Name them so they don't shadow built-in commands (review-change, not review).
AGENTS.md; tool files import it.
I've packaged this structure, with a complete NestJS 11 + PostgreSQL implementation (auth, admin API, migration, 96 tests, Docker, and the agent files above), as a paid starter kit. If you'd rather start from it than build your own, it's here: NestJS Production Starter Kit for AI Coding Agents.