DEV Community

Cover image for How to Structure a NestJS Backend for AI-Assisted Development
Azad Mohamed
Azad Mohamed

Posted on Fully Autonomous

How to Structure a NestJS Backend for AI-Assisted Development

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.

1. One instruction file, and only one

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)
Enter fullscreen mode Exit fullscreen mode
// .gemini/settings.json
{ "context": { "fileName": ["AGENTS.md"] } }
Enter fullscreen mode Exit fullscreen mode

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.

2. Write stop rules, not style advice

"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.
Enter fullscreen mode Exit fullscreen mode

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.

3. Make the architecture boring and stated

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
Enter fullscreen mode Exit fullscreen mode

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:

  • Responses map entities through ResponseDto.fromEntity(); never return an entity (that's how password hashes end up in JSON).
  • User-owned data is looked up by the authenticated ID from @CurrentUser(), never by an ID from the request alone.
  • Clients never set server-owned fields (status, verification state, roles).

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.

4. One error contract

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');
Enter fullscreen mode Exit fullscreen mode

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": "..." }
Enter fullscreen mode Exit fullscreen mode

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.

5. Make verification cheap, or it won't happen

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:

  • Unit tests construct services directly with a small in-memory repository that implements only the TypeORM methods your services use:
  /** 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
  }
Enter fullscreen mode Exit fullscreen mode
  • E2E tests boot the real 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.

6. Enforce the rules that matter; write down the rest

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)"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

7. Demand a fixed report

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.
Enter fullscreen mode Exit fullscreen mode

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.

8. Turn recurring jobs into procedures

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:

  • generate: inspect an existing module first, reuse before creating, add tests, report.
  • database-change: check entity conventions, decide whether a migration is needed, review an existing migration's down(), and never generate one without a request.
  • debug: map symptoms to the right file (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).

The short version

  1. One AGENTS.md; tool files import it.
  2. Stop rules with exact words, not style advice.
  3. One structure, one dependency direction, a few explicit "nevers".
  4. One error class hierarchy and one filter.
  5. Default test suites that need no infrastructure.
  6. Tool-enforced prompts for destructive commands.
  7. A fixed report that separates "verified" from "not performed".
  8. Recurring tasks as procedure files.

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.

Top comments (0)