DEV Community

Olivia Craft
Olivia Craft

Posted on

CLAUDE.md for TypeScript and Node.js: 13 Rules That Make AI Write Type-Safe, Production-Ready Code

CLAUDE.md for TypeScript and Node.js: 13 Rules That Make AI Write Type-Safe, Production-Ready Code

TypeScript exists to catch bugs the compiler can prove. Node.js exists to ship them to production fast. When you let an AI assistant write either, the default output drifts toward any, untyped Express handlers, and validation skipped at the boundary "for now." It compiles. It runs. It explodes when a string shows up where a number was expected.

A CLAUDE.md at the repo root fixes this. It's the file Claude Code reads on every session — the contract that says: this codebase does not accept loose types, throw strings, or trust req.body. Below are 13 rules I've shipped in production TypeScript repos that make AI-generated code merge-ready instead of review-ready.


1. Strict TypeScript config is non-negotiable

Problem: AI defaults to permissive tsconfig.json and silently leans on implicit any.

CLAUDE.md instruction:

tsconfig.json MUST have "strict": true, "noImplicitAny": true, "noUncheckedIndexedAccess": true, and "exactOptionalPropertyTypes": true. Never relax these flags to make code compile. If a flag fights you, fix the type, not the config.

Why it works: Strict mode is the contract. Once Claude knows it can't escape via implicit any or unchecked array access, it generates type guards and narrowing logic up front instead of leaving holes you discover at 2 AM.


2. Ban any — use unknown plus type guards

Problem: When Claude doesn't know a shape, it reaches for any. That single keyword erases every type guarantee downstream.

CLAUDE.md instruction:

any is forbidden. Use unknown for values of unknown shape and narrow with type guards (typeof, in, custom predicates) or zod parsing. The only acceptable any is in a // @ts-expect-error block with a written justification.

Why it works: unknown forces the next reader (or AI iteration) to prove the shape before using it. Type guards become the documentation. The codebase stops accumulating silent escape hatches.


3. Branded types for IDs

Problem: function transfer(from: string, to: string, amount: number) — AI happily swaps from and to because the compiler sees three primitives, not three roles.

CLAUDE.md instruction:

All entity IDs must be branded types: type UserId = string & { readonly __brand: "UserId" }. Construct via a single toUserId() factory. Never pass raw strings to functions expecting an ID. Same rule for currency amounts, timestamps, and slugs.

Why it works: Branded types are zero-cost at runtime but make ID-swap bugs uncompilable. Claude follows the pattern once it sees one branded type defined — it'll brand new IDs without being asked.


4. async/await only — no Promise chains

Problem: AI freely mixes .then() chains with await, producing code where error paths and return values diverge between styles in the same file.

CLAUDE.md instruction:

Use async/await exclusively. No .then(), .catch(), or .finally() chains in application code. The only exception is Promise.all / Promise.allSettled for concurrency. Errors propagate via try/catch, not .catch() callbacks.

Why it works: One async style means one error-handling story. Stack traces become readable. Refactors stop introducing race conditions where a .then() was missed.


5. Validate inputs with zod at every boundary

Problem: AI trusts incoming data because TypeScript types are erased at runtime. req.body is typed as your DTO, but it's actually whatever the client sent.

CLAUDE.md instruction:

Every external input — HTTP requests, environment variables, JSON files, message queue payloads — must be parsed through a zod schema at the boundary. The parsed result is the only typed value that flows inward. No casting req.body as CreateUserDto.

Why it works: zod gives you the type AND the runtime check from one declaration. Once Claude sees a CreateUserSchema.parse(req.body) pattern, it replicates it on every new endpoint instead of trusting incoming JSON.


6. Typed errors, never thrown strings

Problem: throw "user not found" — Claude does this when it's in a hurry. Catch blocks then need String(err) everywhere and the caller can't distinguish a NotFound from a Forbidden.

CLAUDE.md instruction:

Errors must be class instances extending a base AppError with a discriminant code field. Never throw a string, number, or plain object. Catch blocks narrow on err instanceof AppError and switch on code. Unknown errors rethrow.

Why it works: Typed errors give you exhaustiveness checks in switch statements. Logging becomes structured. The HTTP layer maps code → status without sniffing error messages with regex.


7. Barrel exports — use sparingly

Problem: AI loves index.ts files re-exporting everything. They look tidy but break tree-shaking and create circular dependency landmines.

CLAUDE.md instruction:

No barrel index.ts files inside feature modules. Import from the specific file: import { User } from "./user/user.entity", not from "./user". The only barrels allowed are at package boundaries (packages/*/src/index.ts) and must be manually curated, not auto-generated.

Why it works: Direct imports keep the dependency graph shallow and make circular imports a compile error you can locate. Bundlers ship less code. Refactors don't cascade through unrelated modules.


8. Validate environment variables at boot

Problem: process.env.DATABASE_URL! — that ! is a lie. AI uses it because it shuts the compiler up. The app boots, reaches for the URL during the first request, and crashes with undefined is not a function.

CLAUDE.md instruction:

All environment access goes through a single env.ts module that parses process.env with zod at startup and exits the process if validation fails. No process.env.X references anywhere else in the codebase. No non-null assertions on env vars.

Why it works: The app fails to start instead of failing in production. The schema is the documentation of what your service needs. New team members (and Claude) can see the full env contract in one file.


9. Tests use real types — no ts-ignore

Problem: AI reaches for // @ts-ignore in tests when mocking gets awkward. The test passes, the production type drifts, and the test no longer proves anything.

CLAUDE.md instruction:

Test files must compile under the same strict config as production code. No @ts-ignore, no as any, no as unknown as Foo. Use proper test factories that return correctly typed fixtures. If a mock is hard to type, the production interface is wrong.

Why it works: Tests become a second pass of type-checking against your real shapes. When a refactor breaks a test type, you caught a real regression before runtime did.


10. Explicit return types on exported functions

Problem: Claude relies on inference, which means changing an internal helper silently changes the public API of a module.

CLAUDE.md instruction:

All exported functions, route handlers, and class methods must declare explicit return types. Inference is allowed only for local variables and arrow callbacks. Async functions return Promise<T> explicitly, not Promise<void> by accident.

Why it works: Explicit return types lock the contract. A change inside the function body that would alter the inferred return becomes a compile error at the signature, where the breaking change is visible in code review.


11. Dependency injection over direct imports

Problem: import { db } from "./db" inside business logic. Now every test needs to mock the module system, and swapping the database in staging means editing application code.

CLAUDE.md instruction:

Business logic receives dependencies via constructor parameters or function arguments — never imported directly. Side-effect modules (db, http clients, loggers, clocks) are wired in main.ts / server.ts. Domain functions take typed interfaces, not concrete implementations.

Why it works: Tests pass in fakes without jest.mock gymnastics. The dependency graph becomes explicit in function signatures. Claude stops generating code that's only testable via runtime monkey-patching.


12. Typed request and response in Express/Fastify

Problem: app.post("/users", (req, res) => { const body = req.body; ... })body is any. AI proceeds as if it weren't.

CLAUDE.md instruction:

Every route handler must declare typed Request and Response generics (Express: Request<Params, ResBody, ReqBody, Query>; Fastify: schema-typed routes). Bodies, params, and queries are parsed through zod inside the handler. res.json() payloads are constrained by the response type.

Why it works: The HTTP boundary stops being a type hole. Auto-generated OpenAPI docs (via zod-to-openapi or TypeBox) become trustworthy. Frontend clients consuming the API get end-to-end type safety.


13. Path aliases instead of ../../../../

Problem: AI generates import { foo } from "../../../../utils/foo" because that's what the file system suggests. Move the file once and every import breaks.

CLAUDE.md instruction:

Use tsconfig.json paths aliases for cross-feature imports: @/lib/*, @/features/*, @/shared/*. Relative imports allowed only within the same feature folder (max one ../). Configure tsc-alias or your bundler so the aliases resolve at build time.

Why it works: Aliases survive refactors. Imports become readable. The visual depth of ../../../.. no longer hides architectural problems — when you see a cross-feature import, you can name the boundary it crosses.


The pattern

These 13 rules share one premise: TypeScript's value is the contract, not the syntax. AI assistants will happily satisfy the syntax and break the contract. CLAUDE.md is where you write down the contract in language the AI reads on every session, so it stops "helpfully" reaching for any, as, !, and @ts-ignore to make red squiggles disappear.

Drop these rules into your repo, watch the next AI-generated PR, and notice what doesn't show up: untyped handlers, unvalidated bodies, swapped IDs, and process.env.WHATEVER! scattered across the codebase.


Want the full Gist with these 13 rules ready to paste into your project? Grab it here → oliviacraftlat.gumroad.com/l/skdgt

Free, no email gate, MIT-licensed. Fork it, adapt it, ship it.

Top comments (0)