DEV Community

Cover image for Claude Code for NestJS Monorepos: A Practical Setup Guide
Harshit Rathod
Harshit Rathod

Posted on

Claude Code for NestJS Monorepos: A Practical Setup Guide

A practical, step-by-step guide to configuring Claude Code with CLAUDE.md, rules, and skills so AI-generated code matches your team's conventions in a large NestJS monorepo.

Hook: Ask Claude to "add a consumer-visible status to a booking" in an unconfigured monorepo, and it will happily edit the wrong microservice, hardcode an error string, and check user.role === 'admin'. None of that is Claude being wrong — it's Claude guessing. This guide removes the guessing.


Key Takeaways

  • Claude Code reads .claude/ from your git root, so one configuration governs every service in the monorepo.
  • Three mechanisms do the heavy lifting: CLAUDE.md (always-loaded project briefing), rules (hard prohibitions), and skills (multi-step workflows with templates).
  • The single biggest win is service routing — a decision table in CLAUDE.md stops Claude from putting consumer logic in the operator service.
  • Write rules as prohibitions, not preferences — "NEVER query the DB in a loop" is enforced; "prefer bulk queries" is ignored under pressure.
  • Setup is a few hours, one-time, and pays back on every feature: faster scaffolding, fewer convention violations in review, and faster onboarding.

Quick Answer

To set up Claude Code for a NestJS monorepo:
(1) install it with npm install -g @anthropic-ai/claude-code and authenticate once via claude; (2) create a root .claude/CLAUDE.md that documents which service owns what, your request pipeline, and your standard response shape; (3) add a .claude/rules/ folder with hard prohibitions (no N+1 queries, no role-name checks, no relative imports); (4) build a .claude/skills/ folder with multi-step workflows and code templates for repeated tasks like creating an endpoint; and (5) add .claude/settings.json to allow safe commands and deny destructive ones. Commit .claude/ to git so the whole team shares it. The result: Claude generates code that follows your conventions on the first try, instead of guessing.


The Problem: Why Claude Struggles With Large NestJS Projects

A large NestJS monorepo is one of the hardest environments for any AI coding assistant, because correctness depends on conventions that are invisible in any single file. A codebase with dozens of modules, hundreds of DTOs, and custom guards, interceptors, filters, and adapters is genuinely ambiguous. Without context, Claude fills that ambiguity with reasonable defaults — and those defaults won't match your team's conventions.

Concretely, an unconfigured Claude will:

  • Call the database directly from a service instead of going through a repository
  • Hardcode error strings instead of using your ErrorMessages constants
  • Put a new file in the wrong service (booking logic in operator-service when it belongs in api-service)
  • Check user.role === 'admin' instead of using PermissionCodes
  • Use console.log instead of your structured logger
  • Write ../../common/utils instead of the src/ path alias

Why This Problem Exists

These aren't bugs in Claude — they're the predictable result of asking a model to infer team-specific decisions it was never told about. An AI assistant only sees the files it reads in a session; it cannot see why your team removed RolesGuard, that synchronize must stay false, or that two services communicate only over HTTP adapters. Tribal knowledge that lives in your engineers' heads is exactly the knowledge Claude lacks.

The fix is to externalize that tribal knowledge into version-controlled context through three mechanisms, each suited to a different kind of knowledge:

Mechanism What it encodes When Claude uses it
CLAUDE.md Architecture, ownership, conventions Loaded automatically every session
Rules Hard prohibitions and invariants Enforced on every code generation
Skills Multi-step workflows + templates Invoked on demand for repeated tasks

Repository Structure

Before configuring anything, anchor on the layout. A well-structured NestJS monorepo looks like this:

my-backend/
├── .claude/                  # Everything Claude needs to operate well
│   ├── CLAUDE.md             # Root-level instructions (always loaded)
│   ├── rules/                # Enforced coding constraints
│   ├── skills/               # Custom slash commands with deep context
│   └── settings.json         # Command permissions
├── services/
│   ├── api-service/          # NestJS consumer API (port 5001)
│   ├── operator-service/     # NestJS operator/vendor API (port 5002)
│   ├── chat-service/         # Express + Socket.IO (JavaScript, port 5005)
│   └── job-service/          # Scheduled jobs, no HTTP server
├── db/migration/             # Flyway migrations per schema
└── package.json              # Root: only husky + lint-staged
Enter fullscreen mode Exit fullscreen mode

Each service is an independent NestJS app with its own package.json, tsconfig.json, and build output. Inside each service, every feature module follows the same strict three-layer structure:

src/modules/booking/
├── booking.module.ts
├── controllers/    # HTTP routing only, no logic
├── services/       # Business logic and orchestration
├── repositories/   # TypeORM queries only
└── dtos/           # Request/response shapes, validation
Enter fullscreen mode Exit fullscreen mode

Why it matters: This separation is exactly what Claude needs to respect. If it doesn't know the pattern, it mixes layers — services querying the DB directly, controllers calling repositories.

Suggested visual: a diagram of the git-root .claude/ directory applying across all four services.


Step-by-Step: Configuring Claude Code for NestJS

Step 1 — Install Claude Code

npm install -g @anthropic-ai/claude-code
claude   # opens a browser for one-time OAuth
Enter fullscreen mode Exit fullscreen mode

After authenticating, claude works from any directory. Each developer installs and authenticates independently — there's no shared API key.

Monorepo tip: Claude reads .claude/ from the git root, so its instructions apply no matter where you invoke Claude. When working on one service, cd into it (e.g. services/api-service/) so file searches and commands scope correctly.

Step 2 — Create CLAUDE.md

CLAUDE.md is the single most important file you will write — it's the briefing Claude reads at the start of every session. Think of it as onboarding documentation for a senior engineer on day one. Place it at two levels:

  1. Root .claude/CLAUDE.md — service ownership, shared conventions, the request pipeline
  2. Per-service services/{service}/CLAUDE.md — module patterns, auth, and DI specific to that service

The Root CLAUDE.md

  • Problem: Claude can't tell which service should own a new file, so it guesses — and lands consumer logic in the operator service.
  • Solution: A routing decision table plus explicit "never do this" rules.
  • Result: Files land in the correct service on the first pass.
# CLAUDE.md

## Monorepo Structure

Four services under `services/`, each independently runnable.
Run all commands from inside the service directory.

| Service            | Port | Stack                    | Purpose              |
| ------------------ | ---- | ------------------------ | -------------------- |
| `api-service`      | 5001 | NestJS + TypeScript      | Consumer-facing API  |
| `operator-service` | 5002 | NestJS + TypeScript      | Operator/vendor mgmt |
| `chat-service`     | 5005 | Express + Socket.IO + JS | Real-time messaging  |
| `job-service`      | —    | Node.js + TypeScript     | Scheduled jobs       |

## Which Service Owns What

- **operator-service** — if the data describes _what an experience is_:
  package definitions, pricing, taxes, availability, operator accounts.
- **api-service** — if the data describes _a consumer's interaction_:
  bookings, users, auth, trip planning, payments, notifications.

| Task involves...                   | Service                            |
| ---------------------------------- | ---------------------------------- |
| Package categories, pricing, slots | operator-service                   |
| Booking status, user profiles      | api-service                        |
| Stripe payments (consumer side)    | api-service                        |
| Cross-service booking flow         | Start in api-service → IMS adapter |

## Global Request Pipeline (in order)

1. `TraceContextMiddleware` — sets `correlationId` in `AsyncLocalStorage`
2. `ValidationPipe` — whitelist mode, flattens DTO errors
3. `LoggerInterceptor` — logs request/response
4. `TransformInterceptor` — wraps return value in the standard shape
5. Exception filters (`HttpExceptionFilter`, `TypeOrmExceptionFilter`, …)

## Standard Response Shape

Controllers return `{ data, meta }`; `TransformInterceptor` wraps it:
return { data: { items }, meta: { path: request.path } };
// → { success: true, data: { items }, meta: { path }, error: null }

## Cross-Service Communication

api-service NEVER touches operator-service's database. All reads/writes go
through HTTP adapters in `src/common/adapters/` (they handle OAuth refresh and
trace propagation). Need operator data? Call the adapter, never the DB.

## What Claude Must Never Do

- Use `console.log` → use `logMessage()` from `src/common/utils/logger`
- Check `user.role` → use `PermissionCodes` + `@Permissions()`
- Query the DB inside a loop → use bulk queries (`IN`, `JOIN`)
- Use relative `../../` imports → use the `src/` alias
- Set TypeORM `synchronize: true` → Flyway manages all schema changes
Enter fullscreen mode Exit fullscreen mode

What makes a good CLAUDE.md:

  • Decision rules over directory listings. "If the data describes what an experience is, it lives in operator-service" lets Claude resolve ambiguity without asking you.
  • Include the response shape. Otherwise Claude manually wraps responses in every controller, creating inconsistencies.
  • List what NOT to do. "Never use console.log" is more actionable than "use our logger."
  • Document what was removed. When you delete a RolesGuard or a synchronize flag, say why — or Claude will reintroduce it because it looks natural.

Service-Level CLAUDE.md

Each service gets its own file for details too specific for the root:

# api-service/CLAUDE.md

## Module Architecture — never mix layers

controllers/ HTTP routing only — call service, return data
services/ Business logic — orchestrate repos, no direct DB calls
repositories/ TypeORM queries only — accept EntityManager for transactions

## Authentication

- `JwtAuthGuard` validates Bearer tokens, attaches `UserRequest`
- `PermissionsGuard` reads `@Permissions([PermissionCodes.X])`
- Never use RolesGuard or check role names — removed intentionally

## Dependency Injection

Register services by interface token, and inject by token, not class:
{ provide: SERVICE_INTERFACE.BOOKING_SERVICE, useClass: BookingService }

## Background Jobs

BullMQ + Redis. Processors live in `src/modules/{module}/processors/`.
Enqueue via the module's service — never from a controller.
Enter fullscreen mode Exit fullscreen mode

Step 3 — Add Rules

Rules are hard prohibitions — they don't describe architecture, they prevent specific patterns, like an ESLint config for AI-generated code. They live in .claude/rules/ as Markdown files.

Use CLAUDE.md for Use rules/ for
Architecture, module structure Hard prohibitions
Service ownership decisions Patterns Claude must never emit
Conventions, command reference Security / performance invariants

Rule: Prevent N+1 Queries

  • Problem: N+1 queries are easy to introduce and slip through review.
  • Solution: a prohibition rule with a correct bulk-query pattern.
  • Result: Claude writes the bulk query at generation time — the bug is never created.

.claude/rules/database-queries.md

// Wrong — N queries
for (const b of bookings) {
  b.user = await this.userRepo.findOne(b.userId);
}

// Correct — one query
const users = await this.userRepo.find({ where: { id: In(userIds) } });
const userMap = new Map(users.map((u) => [u.id, u]));
bookings.forEach((b) => {
  b.user = userMap.get(b.userId);
});
Enter fullscreen mode Exit fullscreen mode

Rule: Permission Checks

.claude/rules/permissions.md

// Wrong
if (user.roles.includes('admin')) { ... }

// Correct
@Permissions([PermissionCodes.MANAGE_BOOKINGS])
@UseGuards(JwtAuthGuard, PermissionsGuard)
Enter fullscreen mode Exit fullscreen mode

Why: role names get merged, split, and renamed; checks against them break
silently. PermissionCodes are stable constants that survive role
restructuring. Import from src/common/constants/permissions.constant.ts.

Rule: Import Style

.claude/rules/imports.md

// Wrong
import { UserService } from "../../services/user.service";
// Correct
import { UserService } from "src/modules/user/services/user.service";
Enter fullscreen mode Exit fullscreen mode

The src/ alias is configured in tsconfig.json (baseUrl: "./") and Jest's
moduleNameMapper. Use it everywhere.

Other Rules Worth Defining

The same prohibition pattern applies to many recurring NestJS pitfalls. A few more rules worth their own file:

  • Error handling — always throw from ErrorMessages/ResponseCodeKeys constants and use NestJS exceptions; never hardcode strings or return raw error objects.
  • Logging — use the structured logMessage() helper at the right level; never console.log.
  • Transactions — multi-write operations must run in a single EntityManager transaction; never fire independent writes that can half-commit.
  • DTO validation — every request DTO needs class-validator decorators; the ValidationPipe whitelist drops anything undecorated, so missing decorators silently lose data.
  • Entity ↔ migration parity — any entity change needs a matching Flyway migration; synchronize stays false.
  • Swagger coverage — every endpoint carries @ApiSpec + a typed response DTO so the generated docs stay accurate.

Step 4 — Build Skills

Skills are project-specific slash commands that bundle a workflow, code templates, and references — they turn a repeated multi-file task into one command. Write one for any repeated, multi-step task: building an endpoint, writing a migration, scaffolding a module, reviewing a PR.

Skill Structure

.claude/skills/api-development/
├── SKILL.md                    # Workflow, trigger, steps
├── assets/
│   ├── controller-template.md
│   ├── service-template.md
│   ├── repository-template.md
│   └── dto-template.md
└── references/
    ├── nestjs-conventions.md
    └── swagger-documentation.md
Enter fullscreen mode Exit fullscreen mode

Writing SKILL.md

# API Development Skill

## Trigger

Invoke with `/api-development` when building or modifying a NestJS endpoint.

## Workflow

1. **Determine service & module** — check CLAUDE.md routing; ask if ambiguous.
2. **Read what exists** — entities, repositories, DTOs already in the module.
3. **Generate the DTO first** — it defines the contract. class-validator
   decorators; extend `PaginationRequestDTO` for lists.
4. **Generate the repository** — inject `DataSource`; accept an optional
   `EntityManager` for transactions; queries only, no logic.
5. **Generate the service** — inject the repository interface token; use
   `ErrorMessages` constants; throw `NotFoundException` etc.
6. **Generate the controller**`@ApiSpec` + `@ApiOkResponse`; return
   `{ data, meta }`; no business logic.
7. **Wire the module** — add to `providers`, export by interface token.
Enter fullscreen mode Exit fullscreen mode

Asset Template (example)

Templates should be real, compilable code — not pseudocode. assets/controller-template.md:

@ApiTags("bookings")
@Controller("bookings")
export class BookingController {
  constructor(
    @Inject(SERVICE_INTERFACE.BOOKING_SERVICE)
    private readonly bookingService: IBookingService,
  ) {}

  @Post()
  @UseGuards(JwtAuthGuard, PermissionsGuard)
  @Permissions([PermissionCodes.CREATE_BOOKING])
  @ApiSpec({ summary: "Create a new booking" })
  @ApiCreatedResponse({ type: CreateBookingSuccessDto })
  async createBooking(
    @Body() dto: CreateBookingDto,
    @Req() req: UserRequest,
  ): Promise<{ data: BookingEntity }> {
    const booking = await this.bookingService.createBooking(dto, req.user.id);
    return { data: booking };
  }
}
Enter fullscreen mode Exit fullscreen mode

With this skill, a complete endpoint (controller + service + repository + DTOs, wired into the module) takes under 3 minutes instead of ~20 — and matches your conventions on the first try.

Other Skills Worth Building

Any repeated, multi-step workflow with team-specific conventions is a skill candidate. Beyond endpoint scaffolding:

  • Migration generator — scaffold a Flyway migration in the right schema folder with your versioning and naming convention, plus the matching entity change.
  • Module scaffolder — generate a full module skeleton (module file, three layers, DI tokens) wired and ready.
  • PR review — review a diff against your conventions: N+1 checks, permission usage, layer boundaries, missing tests.
  • Test generator — produce *.spec.ts files following your mocking and fixture patterns for a given service.
  • Cross-service adapter — generate a new IMS adapter method plus its response interface and transform helper.
  • Use-case / BDD docs — turn a requirement into user stories with Given/When/Then acceptance criteria.

Step 5 — Configure Settings & Permissions

.claude/settings.json controls which Bash commands run without a permission prompt. Allow read-only and safe test/lint operations freely; deny anything destructive.

{
  "permissions": {
    "allow": [
      "Bash(npm run lint:*)",
      "Bash(npm run test:*)",
      "Bash(npm run build)",
      "Bash(npx jest *)"
    ],
    "deny": ["Bash(rm -rf *)", "Bash(git push *)", "Bash(git reset --hard *)"]
  }
}
Enter fullscreen mode Exit fullscreen mode
File Applies to Committed
.claude/settings.json Everyone on the team Yes
.claude/settings.local.json You only (overrides) No (gitignored)

Best Practices

Best Practice callout: Treat .claude/ as production code. It ships your conventions to every engineer and every AI session — review it, version it, and prune it like any other source.

  • Keep CLAUDE.md under ~300 lines. Context has a cost; a bloated file crowds out the code Claude needs to read. If it's derivable from the code, cut it.
  • Write rules as prohibitions, not guidance. "NEVER call the DB in a loop" is enforced; "prefer bulk queries" is ignored under pressure.
  • Put templates in skills, not CLAUDE.md — they should load only when the skill is invoked, not on every session.
  • Version .claude/ in git. It's part of your codebase: same review, same history. New rules get PRs; stale rules get deleted.
  • Use decision tables liberally. Anywhere two reasonable choices exist — DTO placement, entity location, service boundaries — a table beats prose.

Recommended .claude/ layout

.claude/
├── CLAUDE.md                  # Root guide (<300 lines)
├── settings.json              # Team permissions (committed)
├── rules/
│   ├── database-queries.md    # No N+1
│   ├── permissions.md         # PermissionCodes, no role checks
│   └── imports.md             # src/ alias only
└── skills/
    └── api-development/
        ├── SKILL.md
        ├── assets/            # controller/service/repository/dto templates
        └── references/        # conventions, swagger
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

⚠️ Each of these silently degrades output quality — Claude will look like it's working while quietly violating your conventions.

  1. Writing CLAUDE.md as a tutorial. Don't explain what NestJS is — Claude knows. Document only what's unique to your project: module names, error constants, response shape, permission system.
  2. Omitting negative rules. Telling Claude what not to do prevents whole classes of mistakes. The expensive errors are the ones that look correct but violate your patterns.
  3. Cramming everything into the root CLAUDE.md. It loads in every session. Split service-specific depth into per-service files so it doesn't pollute context when you're elsewhere.
  4. No routing decision table. Wrong-service placement comes from ambiguity, not ignorance. A table makes the decision deterministic.
  5. Forgetting the cross-service adapter pattern. In an HTTP-linked monorepo, Claude will try to import across services or query the other DB. Spell out that all cross-service calls go through adapters.
  6. Not mentioning synchronize: false. Claude assumes TypeORM manages the schema. If you use Flyway, entity changes without a matching migration fail silently — say so explicitly.

Real-World Example: Adding a Consumer-Visible Booking Status

A walkthrough of how a configured setup changes one real task.

The request: "Add a consumer-visible status to a booking package."

Without configuration, Claude reasons: "packages live in operator-service," and edits the package entity there — the wrong service. It hardcodes the status string, and adds an if (user.roles.includes('admin')) guard.

With configuration, here's the chain:

  1. Routing table in CLAUDE.md tells Claude that consumer-visible status describes a consumer interaction → it belongs in api-service, not operator-service.
  2. Cross-service rule reminds it the operator's package definition is read via an IMS adapter, so it adds the field to the adapter's transform — not by reaching into the other DB.
  3. /api-development skill scaffolds the DTO, repository method, service logic, and controller in the right order.
  4. Permission rule makes it emit @Permissions([PermissionCodes.VIEW_BOOKING_STATUS]) instead of a role check.
  5. Error rule makes it throw NotFoundException with an ErrorMessages constant.

One ambiguous sentence, resolved into correct, convention-matching code across four files — without a single clarifying question.


Results & Metrics

A few hours of one-time setup changes the day-to-day in measurable ways:

Before After
Review every generated file for hardcoded strings, wrong imports, role checks Rules make those impossible — review focuses on logic, not conventions
New endpoint ≈ 20 min wiring four layers by hand /api-development scaffolds + wires it in under 3 min
Files landed in the wrong service, caught late in review Routing table places them correctly on the first pass
N+1 queries and console.log slipped into PRs Caught at generation time, never written
New hires guessed at module boundaries and patterns CLAUDE.md teaches the conventions implicitly on day one

Compounding wins that don't show up in a single PR:

  • Consistency. Code generated months apart looks the same, because it comes from the same templates and rules — not from whatever Claude inferred that day.
  • Less review fatigue. Reviewers stop flagging the same convention violations and spend attention on actual design.
  • Faster onboarding. Routing tables and rules are documentation that also executes — a new engineer's first AI-assisted feature already follows house style.
  • Lower context cost. A tight CLAUDE.md plus on-demand skills means Claude spends its context budget reading your code, not re-deriving conventions every session.

Extractable insight: The investment pays for itself within the first few features and keeps paying on every one after — because the cost is one-time and the benefit recurs on every task.


FAQ

What is CLAUDE.md?
CLAUDE.md is a Markdown file Claude Code reads automatically at the start of every session. It briefs the AI on your project's architecture, conventions, and prohibitions — the equivalent of onboarding docs for a new engineer.

Where should CLAUDE.md go in a monorepo?
Put a root .claude/CLAUDE.md at the git root for cross-cutting concerns (service ownership, shared conventions), and a CLAUDE.md inside each service for service-specific patterns. Claude reads the root first, then the service-level file when you work in that directory.

What's the difference between CLAUDE.md and rules?
CLAUDE.md describes architecture and conventions; rules (in .claude/rules/) are hard prohibitions Claude must never violate. Use CLAUDE.md for "here's how things work" and rules for "never do this," such as banning N+1 queries or role-name checks.

How do Claude Code skills work?
A skill is a project-specific slash command (e.g. /api-development) backed by a SKILL.md workflow plus code templates and references. Invoking it makes Claude follow a deterministic, multi-step process — like scaffolding a full NestJS endpoint across four files.

How long should CLAUDE.md be?
Aim for under ~300 lines. Context is finite, and a bloated file crowds out the actual code Claude needs to read. Document only what isn't derivable from the codebase itself.

Does Claude Code work with a NestJS monorepo of multiple services?
Yes. Claude reads .claude/ from the git root, so one configuration governs every service. A routing decision table in CLAUDE.md tells Claude which service owns which kind of data.

How do I stop Claude from writing N+1 queries or role-based permission checks?
Add prohibition rules in .claude/rules/. A database-queries.md rule bans DB calls inside loops; a permissions.md rule bans user.role checks in favor of PermissionCodes and @Permissions().

Should I commit the .claude directory to git?
Yes — commit .claude/CLAUDE.md, rules/, skills/, and settings.json so the whole team shares the same configuration. Keep personal overrides in .claude/settings.local.json, which is gitignored.


Key Takeaways (Recap)

  • Claude struggles in large NestJS monorepos because correctness depends on conventions invisible in any single file.
  • Externalize that knowledge with CLAUDE.md (briefing), rules (prohibitions), and skills (workflows) — all committed to git.
  • The service-routing decision table is the highest-leverage thing you can write.
  • Rules phrased as prohibitions are enforced; preferences are not.
  • Setup is a few hours, one-time, and compounds across every feature and every new hire.

Conclusion

Setting up Claude Code for a NestJS monorepo isn't about making Claude smarter — it's about eliminating guesswork. A codebase with dozens of modules, hundreds of DTOs, and custom guards and adapters is genuinely ambiguous without context, and Claude's reasonable defaults won't match your conventions.

The investment is small and one-time: a focused CLAUDE.md for routing and prohibitions, a few rules files for hard constraints, and one or two skills for your most repeated tasks. After that, Claude generates code that looks like your team wrote it — not like someone who just read the NestJS docs.


Further Reading


Patterns here are drawn from a production NestJS monorepo running four services: a consumer API, an operator management system, a real-time chat service, and a scheduled job runner. The approach scales to any NestJS project with conventions worth preserving.

Top comments (0)