{"slug": "how-to-structure-a-nestjs-backend-for-ai-assisted-development", "title": "How to Structure a NestJS Backend for AI-Assisted Development", "summary": "A developer outlined a NestJS backend structure designed for AI coding agents, arguing that repository conventions—not better prompts—keep agents from scattering queries into controllers, leaking entities, or running migrations unprompted. The approach centralizes rules in a single AGENTS.md (imported by CLAUDE.md and referenced by Gemini CLI settings), enforces a Controller → Service → Repository dependency rule, standardizes errors through a single AppException hierarchy and global filter with stable UPPER_SNAKE codes, and keeps default test suites infrastructure-free so agents actually run them.", "body_md": "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.\n\nThe 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.\n\nEvery 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:\n\n``` php\n<!-- CLAUDE.md -->\n@AGENTS.md\n\n## Claude Code specifics\n(skills, subagents, permission rules: only what is genuinely Claude-specific)\n// .gemini/settings.json\n{ \"context\": { \"fileName\": [\"AGENTS.md\"] } }\n```\n\nRules 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.\n\n\"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:\n\n```\n## Stop rules\n\n1. Do not generate, run or revert migrations until the user tells you to in this conversation.\n   An entity change is not permission. Make the code change and report:\n   \"Schema change detected. Migration required. Not generated or run: no explicit approval.\"\n2. Do not run or edit seeds until the user tells you to.\n3. Do not format code. The pre-commit hook formats staged files; CI checks formatting.\n```\n\nRule 3 is a cost rule: formatting runs burn agent time and produce noisy diffs, and Husky + lint-staged already do it on every commit.\n\nAgents copy what they see. If the codebase has one pattern, they copy that one. Pick an explicit structure and write it down:\n\n```\nsrc/<feature>/\n  controllers/   HTTP only: routing, guards, Swagger decorators\n  services/      business rules; one class per responsibility\n  dto/           request DTOs (class-validator, length limits) and response DTOs\n  entities/      the data model; no queries, HTTP, email or JWT logic\n```\n\nThen 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:\n\n`ResponseDto.fromEntity()`; never return an entity (that's how password hashes end up in JSON).`@CurrentUser()`, never by an ID from the request alone.\nA global `ValidationPipe` with `whitelist: true, forbidNonWhitelisted: true` backs the last rule up mechanically: a DTO that doesn't declare `status` can't receive it.\n\nGive the agent exactly one way to fail:\n\n```\nexport abstract class AppException extends Error { /* status + stable code */ }\nexport class NotFoundError extends AppException {}\nexport class ConflictError extends AppException {}\n// ...\n\nthrow new ConflictError('EMAIL_ALREADY_REGISTERED', 'Email is already registered');\n```\n\nA single global exception filter turns every error, including unexpected ones, into the same shape:\n\n```\n{ \"statusCode\": 409, \"code\": \"EMAIL_ALREADY_REGISTERED\", \"message\": \"...\",\n  \"path\": \"/api/v1/auth/register\", \"requestId\": \"6f1c...\", \"timestamp\": \"...\" }\n```\n\nStable `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.\n\nAn 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:\n\n```\n  /** Minimal in-memory stand-in for the TypeORM repository methods used by the services. */\n  export class MockRepository<T extends { id: string }> {\n    readonly items = new Map<string, T>();\n    create(data: Partial<T> = {}): T { /* ... */ }\n    async save(entity: Partial<T>): Promise<T> { /* ... */ }\n    // findOne, findAndCount, update... only what's used\n  }\n```\n\n`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.\nThen 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.\n\nBe 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.\n\nWritten 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:\n\n```\n{\n  \"permissions\": {\n    \"ask\": [\"Bash(npm run migration:*)\", \"Bash(npm run seed *)\", \"Bash(psql *)\",\n            \"Bash(docker compose down *)\", \"Bash(git push *)\"],\n    \"deny\": [\"Read(**/.env)\", \"Read(**/.env.production)\"]\n  }\n}\n```\n\nKnow 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.\n\nThe most useful line in my `AGENTS.md` is the report format:\n\n```\nImplemented       what changed and why\nFiles changed     paths\nVerification      commands actually run and their actual results\nNot performed     e.g. migration generation, seed execution, Docker, deploy\nUnverified        anything you could not check\nNever claim a command passed if you did not run it.\n```\n\nIt 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.\n\nWhen 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:\n\n`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.\nName them so they don't shadow built-in commands (`review-change`, not `review`).\n\n`AGENTS.md`; tool files import it.\n*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](https://azadhmhd.gumroad.com/l/nestjs-production-starter-kit?utm_source=devto&utm_medium=article&utm_campaign=launch_v1&utm_content=structure_article).*", "url": "https://wpnews.pro/news/how-to-structure-a-nestjs-backend-for-ai-assisted-development", "canonical_source": "https://dev.to/azadhmhd/how-to-structure-a-nestjs-backend-for-ai-assisted-development-4b7f", "published_at": "2026-10-17 20:05:56+00:00", "updated_at": "2026-10-17 20:06:06.180140+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["NestJS", "Claude Code", "GitHub Copilot", "Cursor", "Gemini CLI", "Codex"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-structure-a-nestjs-backend-for-ai-assisted-development", "markdown": "https://wpnews.pro/news/how-to-structure-a-nestjs-backend-for-ai-assisted-development.md", "text": "https://wpnews.pro/news/how-to-structure-a-nestjs-backend-for-ai-assisted-development.txt", "jsonld": "https://wpnews.pro/news/how-to-structure-a-nestjs-backend-for-ai-assisted-development.jsonld"}}