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.mdstops 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
ErrorMessagesconstants - Put a new file in the wrong service (booking logic in
operator-servicewhen it belongs inapi-service) - Check
user.role === 'admin'instead of usingPermissionCodes - Use
console.loginstead of your structured logger - Write
../../common/utilsinstead of thesrc/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
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
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
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,cdinto 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:
-
Root
.claude/CLAUDE.md— service ownership, shared conventions, the request pipeline -
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
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
RolesGuardor asynchronizeflag, 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.
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);
});
Rule: Permission Checks
.claude/rules/permissions.md
// Wrong
if (user.roles.includes('admin')) { ... }
// Correct
@Permissions([PermissionCodes.MANAGE_BOOKINGS])
@UseGuards(JwtAuthGuard, PermissionsGuard)
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";
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/ResponseCodeKeysconstants and use NestJS exceptions; never hardcode strings or return raw error objects. -
Logging — use the structured
logMessage()helper at the right level; neverconsole.log. -
Transactions — multi-write operations must run in a single
EntityManagertransaction; never fire independent writes that can half-commit. -
DTO validation — every request DTO needs
class-validatordecorators; theValidationPipewhitelist drops anything undecorated, so missing decorators silently lose data. -
Entity ↔ migration parity — any entity change needs a matching Flyway migration;
synchronizestaysfalse. -
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
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.
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 };
}
}
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.tsfiles 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 *)"]
}
}
| 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
Common Mistakes
⚠️ Each of these silently degrades output quality — Claude will look like it's working while quietly violating your conventions.
- 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.
- 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.
- 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.
- No routing decision table. Wrong-service placement comes from ambiguity, not ignorance. A table makes the decision deterministic.
- 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.
-
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:
-
Routing table in
CLAUDE.mdtells Claude that consumer-visible status describes a consumer interaction → it belongs inapi-service, notoperator-service. - 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.
-
/api-developmentskill scaffolds the DTO, repository method, service logic, and controller in the right order. -
Permission rule makes it emit
@Permissions([PermissionCodes.VIEW_BOOKING_STATUS])instead of a role check. -
Error rule makes it throw
NotFoundExceptionwith anErrorMessagesconstant.
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
- Claude Code documentation — official setup, configuration, and best-practices reference
- NestJS documentation — providers, guards, interceptors, and module architecture
- TypeORM · class-validator · Flyway — the data, validation, and migration tooling referenced above
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)