Nova Labs is currently on pause. New product purchases are unavailable. The blog remains live as an archive of the experiment.
Back to blog

CLAUDE.md for TypeScript and Node.js: the complete configuration guide

April 11, 2026 12 min read

TypeScript projects have more configuration surface area than almost any other stack. You have tsconfig settings, path aliases, module resolution modes, multiple package managers, several competing test runners, and a build toolchain that varies wildly between projects. Without a CLAUDE.md that covers this ground, Claude makes a lot of reasonable-but-wrong assumptions.

This guide covers exactly what to put in your CLAUDE.md for TypeScript and Node.js projects, with real config examples for each section. If you want to skip the reading, use the free ContextKit generator - it handles TypeScript projects with a guided wizard.

Why TypeScript projects need a detailed CLAUDE.md

In a Python project, Claude mostly needs to know your framework and testing setup. TypeScript adds several layers of configuration that meaningfully change how correct code looks:

  • Strict mode changes which types are valid
  • Path aliases change all import statements
  • Module resolution (Node vs Bundler) changes how imports resolve
  • ESM vs CommonJS changes how you write imports and exports
  • The framework (Next.js, Astro, Express, Fastify) changes the entire pattern set

Without these in CLAUDE.md, Claude writes code that compiles in isolation but doesn't fit your project. The import paths are wrong, strict types are violated, and the patterns don't match your framework.

1. Project structure

Start with what you have: monorepo or single package, and your directory layout. This tells Claude where to look for existing code and where to put new files.

Single package

## Project Structure

Single package TypeScript project.

src/
  index.ts          # Entry point
  lib/              # Core library code
  types/            # Shared TypeScript interfaces and types
  utils/            # Pure utility functions
  __tests__/        # Test files (or co-located .test.ts files)
dist/               # Compiled output (never modify directly)
tsconfig.json
package.json

Monorepo (pnpm workspaces)

## Project Structure

pnpm monorepo with workspace packages.

packages/
  api/              # Express API server (Node.js, CommonJS)
  web/              # React frontend (Vite, ESM)
  shared/           # Shared types and utilities (dual CJS/ESM)
  config/           # Shared tsconfig and eslint configs
apps/
  docs/             # Documentation site

Each package has its own tsconfig.json extending from packages/config/tsconfig.base.json.
Never import across packages with relative paths - use workspace package names (e.g., @myproject/shared).
Build order: shared -> api, web (in parallel).

The monorepo case is worth being explicit about. Claude defaults to relative paths between packages if you don't tell it otherwise, which breaks workspace resolution.

2. TypeScript configuration

You don't need to paste your full tsconfig into CLAUDE.md - Claude can read the file when needed. What you do need is a summary of the settings that change how Claude writes code.

## TypeScript Config

Strict mode on. Key settings that affect how you write code:

- strict: true (noImplicitAny, strictNullChecks, strictFunctionTypes, all on)
- target: ES2022
- module: NodeNext (for the API package)
- moduleResolution: NodeNext
- noUncheckedIndexedAccess: true
- exactOptionalPropertyTypes: true

Path aliases (configured in tsconfig.json, resolved by tsconfig-paths at runtime):
  @/* -> src/*
  @shared/* -> ../../packages/shared/src/*

Never use 'any'. Use 'unknown' for values of uncertain type, then narrow.
When a type assertion is necessary, use 'as Type' with a comment explaining why.

The noUncheckedIndexedAccess setting is worth calling out explicitly because it's non-default and changes a lot of code - array access returns T | undefined instead of T, which requires null checks Claude won't add unless it knows this is on.

Path aliases deserve special attention

This is the single most common source of wrong imports from Claude. If your project uses @/components/Button instead of ../../components/Button, say so clearly and show the mapping. Show a correct import example:

## Import Conventions

Use path aliases, never deep relative imports:

// Correct
import { Button } from '@/components/ui/Button';
import type { User } from '@/types/user';
import { formatDate } from '@/lib/utils';

// Wrong - do not use relative paths that go more than one level up
import { Button } from '../../../components/ui/Button';

3. Package manager

Specify your package manager and include the commands Claude will actually run. Claude defaults to npm when it's unsure, and running npm install in a pnpm workspace creates the wrong lockfile.

## Package Manager

pnpm (version 9). Never use npm or yarn in this project.

Install a package: pnpm add <package>
Install dev dep: pnpm add -D <package>
Install workspace dep: pnpm add <package> --filter @myproject/api
Run script: pnpm --filter @myproject/api dev
Run all: pnpm -r build

Do not run 'npm install' or 'yarn add' - this will create the wrong lockfile.

If you use Bun, the lockfile situation is different. Since Bun 1.1, the lockfile is bun.lock (text-based), replacing the old binary bun.lockb:

## Package Manager

Bun (latest stable). Never use npm or pnpm.

Install: bun add <package>
Install dev: bun add -d <package>
Run: bun run <script>
Execute: bunx <package>

Bun uses bun.lock (text-based since Bun 1.1). Do not run npm install or pnpm install -
this creates a package-lock.json or pnpm-lock.yaml that conflicts with the Bun setup.

4. Testing

TypeScript projects have three mainstream test runners right now - Jest, Vitest, and (for E2E) Playwright. Each has different import conventions and configuration. Tell Claude which one you use and how to run it.

Vitest

## Testing

Vitest for unit and integration tests.

Run all tests: pnpm test
Run with UI: pnpm test --ui
Run a specific file: pnpm test src/lib/format.test.ts
Coverage: pnpm test --coverage

Test file naming: [filename].test.ts, co-located with source files.
Not in a separate __tests__/ folder.

Vitest globals are not enabled - import describe, it, expect from 'vitest':

  import { describe, it, expect, vi } from 'vitest';

Use vi.fn() for mocks (not jest.fn()). Use vi.spyOn() for spies.
For async tests, use vi.useFakeTimers() when testing timers.

Test structure:
  describe('FunctionName', () => {
    it('should [expected behavior] when [condition]', () => {
      // arrange
      // act
      // assert
    });
  });

Jest with ts-jest

## Testing

Jest with ts-jest transformer.

Run all tests: npx jest
Run single file: npx jest src/lib/format.test.ts
Watch mode: npx jest --watch
Coverage: npx jest --coverage

Test files: *.test.ts, co-located with source files.
Config in jest.config.ts (not jest.config.js).

Import from '@jest/globals' explicitly (globals are disabled):

  import { describe, it, expect, jest } from '@jest/globals';

Module name mapper in jest.config.ts handles path aliases - check the config
before writing imports in test files.

Mock modules: jest.mock('../path/to/module')
Reset mocks between tests: configured in jest.config.ts (clearMocks: true)

Playwright for E2E

## E2E Testing

Playwright for end-to-end tests.

Tests in: e2e/ directory (not mixed with unit tests)
Run: npx playwright test
Run with UI: npx playwright test --ui
Debug: npx playwright test --debug

Test files: *.spec.ts

Base URL configured in playwright.config.ts (http://localhost:3000).
Start the dev server before running e2e tests (or use webServer config in playwright.config.ts).

Use page.getByRole() and page.getByLabel() over CSS selectors.
Never use page.waitForTimeout() - use page.waitForSelector() or proper assertions.

import { test, expect } from '@playwright/test';

5. Linting and formatting

Tell Claude your formatter and linter, the commands to run them, and any rules that affect how code is written. This prevents Claude from writing code that immediately fails your CI.

ESLint + Prettier

## Linting & Formatting

ESLint for linting, Prettier for formatting.

Lint: pnpm lint (eslint . --ext .ts,.tsx)
Format: pnpm format (prettier --write .)
Check format: pnpm format:check

Key ESLint rules in effect:
  - @typescript-eslint/no-explicit-any: error
  - @typescript-eslint/explicit-function-return-type: warn (on public functions)
  - @typescript-eslint/no-unused-vars: error
  - no-console: warn (use the logger utility instead)
  - import/order: enforced (external, then internal, then relative)

Prettier config (in .prettierrc):
  - singleQuote: true
  - trailingComma: 'es5'
  - semi: true
  - printWidth: 100

Always write code that passes the linter. Run 'pnpm lint' before declaring a task done.

Biome (replaces ESLint + Prettier)

## Linting & Formatting

Biome for both linting and formatting (replaces ESLint and Prettier).
Config in biome.json.

Lint + format: pnpm biome check --apply .
Check only: pnpm biome check .
Format only: pnpm biome format --write .

Biome is strict on:
  - No any types
  - No non-null assertions (!.) without justification
  - Consistent import ordering
  - No unused imports or variables

Use Biome's opinionated defaults. Do not fight the formatter - write code that
Biome accepts, not code that you then have to auto-fix.

6. Build tooling

Mention your build tool and the output it produces. This matters because Claude sometimes tries to reference build outputs in source code, or runs the wrong build command.

Vite

## Build

Vite for development and production builds.

Dev server: pnpm dev (http://localhost:5173)
Production build: pnpm build (outputs to dist/)
Preview built output: pnpm preview

Vite handles TypeScript transpilation. Do not run tsc separately for the browser build.
tsc is used only for type checking: pnpm typecheck (tsc --noEmit)

Environment variables: prefix with VITE_ to expose to the browser.
  VITE_API_URL is available in code as import.meta.env.VITE_API_URL
  Never use process.env in browser code.

tsc for Node.js

## Build

tsc for compilation (Node.js API server).

Compile: pnpm build (tsc -p tsconfig.build.json)
Output: dist/ (mirrors src/ structure)
Watch: pnpm build:watch

Do not edit files in dist/. Always edit src/ and recompile.
tsconfig.json is for editor/type-checking. tsconfig.build.json is for compilation
(excludes test files, includes only src/).

Production: node dist/index.js
Development (with ts-node): pnpm dev

esbuild or tsup

## Build

tsup (esbuild-based) for library builds.

Build: pnpm build
Outputs:
  dist/index.js       # CJS bundle
  dist/index.mjs      # ESM bundle
  dist/index.d.ts     # Type declarations

tsup config in tsup.config.ts. Do not modify dist/ directly.
When adding new entry points, update tsup.config.ts and the package.json exports field.

7. Framework-specific sections

Add a framework section if you're using one. Each framework has patterns Claude needs to know to write correct code.

Next.js App Router

## Framework: Next.js 14 App Router

Key conventions:
- Server Components by default. Add 'use client' only when needed (browser APIs, hooks, interactivity)
- 'use server' for Server Actions (form submissions, mutations)
- Data fetching: fetch() with cache options in Server Components, not useEffect
- Loading states: loading.tsx files, not manual loading state
- Error boundaries: error.tsx files at the appropriate layout level

File conventions:
  app/             # Routes (folder = route segment)
  app/page.tsx     # Route page (always default export)
  app/layout.tsx   # Layout (always default export)
  app/loading.tsx  # Suspense loading state
  app/error.tsx    # Error boundary
  app/not-found.tsx

Metadata: export const metadata or generateMetadata from page/layout files.
Route handlers: app/api/[route]/route.ts, export GET, POST, etc.

Never use the pages/ router - this project uses App Router only.

Express

## Framework: Express + TypeScript

Pattern: router -> middleware -> controller -> service -> repository

src/
  routes/       # Express routers (one file per resource)
  middleware/   # Request middleware (auth, validation, logging)
  controllers/  # Request/response handling (thin, delegates to services)
  services/     # Business logic
  repositories/ # Database access
  types/        # Express augmentations and shared types

Controllers handle HTTP concerns only. No business logic in controllers.
Services do not import from express - no req/res in service layer.

Error handling: throw typed errors in services, catch in a central error middleware.
Don't use try/catch in every route handler.

Type augmentation for req.user:
  // src/types/express.d.ts
  declare global {
    namespace Express {
      interface Request {
        user?: AuthUser;
      }
    }
  }

Fastify

## Framework: Fastify + TypeScript

Use Fastify's type providers for request/response typing:

import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';

Routes are plugins - one file per resource, registered with fastify.register().
Schema validation via Zod (not JSON Schema directly).

src/
  plugins/      # Fastify plugins (db, auth, cors - registered in app.ts)
  routes/       # Route handlers as plugins
  schemas/      # Zod schemas for request/response
  services/     # Business logic
  app.ts        # Fastify instance setup

Always use typed route handlers:
  fastify.get<{ Params: ParamsType; Reply: ReplyType }>('/path', handler)

Do not use fastify.route() with inline schemas - use Zod schemas imported from /schemas.

Astro

## Framework: Astro 4

src/
  pages/        # File-based routing (.astro files = pages)
  layouts/      # Page layouts
  components/   # Reusable components (.astro or framework components)
  content/      # Content collections (Markdown/MDX)
  styles/       # Global CSS

Astro components (.astro) for static content.
React/Svelte islands only when interactivity is needed (client:load, client:visible).
Content collections via defineCollection and getCollection.

TypeScript is configured in tsconfig.json extending astro/tsconfigs/strict.
Use Astro.props for component props - define Props interface in the component frontmatter.

Images: use the  component from 'astro:assets' for optimization.
Links: use relative paths for internal links.

8. Common mistakes Claude makes in TypeScript projects

These are the issues that come up most often. Adding rules for them to your CLAUDE.md reduces the frequency significantly.

The any type

Claude reaches for any when it's unsure about a type, when handling errors, and when working with external data. The fix is explicit:

## TypeScript Rules

No any types. Alternatives:
- Use 'unknown' for values of uncertain type, then narrow with type guards
- Use proper generics instead of any for flexible functions
- Use proper types for caught errors:
    try { ... } catch (error) {
      if (error instanceof Error) { ... }
    }
- For third-party libraries without types: create a .d.ts file, don't use any

If you genuinely can't type something, use 'as unknown as Type' with a comment
explaining why - this is explicit and searchable, unlike any.

Wrong import paths

Claude generates imports based on what it knows about your structure. If path aliases aren't documented, it defaults to relative paths. If the alias mapping is wrong, imports break at runtime even if TypeScript compiles.

## Imports

This project uses path aliases. Always check the alias mapping before writing imports.

Configured aliases (from tsconfig.json):
  @/* maps to ./src/*
  @api/* maps to ./packages/api/src/*
  @shared/* maps to ./packages/shared/src/*

Correct:
  import { createUser } from '@/services/users';
  import type { User } from '@shared/types';

Wrong:
  import { createUser } from '../../services/users';
  import type { User } from '../../../packages/shared/src/types';

Wrong module resolution

The difference between moduleResolution: Node and moduleResolution: NodeNext (or Bundler) affects import syntax. With NodeNext and ESM, you need explicit file extensions on imports. Claude often generates extension-less imports that break in strict ESM mode.

## Module System

This project uses ESM (type: "module" in package.json) with moduleResolution: NodeNext.

Import conventions for ESM + NodeNext:
- Relative imports require explicit .js extension (TypeScript resolves .ts but emits .js):
    import { helper } from './utils.js';      // Correct
    import { helper } from './utils';          // Wrong - breaks at runtime
- Importing types: use 'import type' to avoid runtime issues
    import type { User } from './types.js';
- Path aliases do not need extensions (resolved by bundler/tsconfig-paths)

If you're unsure: check an existing import in the codebase and follow its pattern.

Incorrect generic usage

Claude sometimes writes overly specific generic constraints, or forgets generics entirely and uses any as a shortcut. Set the standard:

## Generics

Prefer generics over any for flexible functions:

// Correct
function first(arr: T[]): T | undefined {
  return arr[0];
}

// Wrong
function first(arr: any[]): any {
  return arr[0];
}

Constrain generics when the constraint matters:
  function getProperty(obj: T, key: K): T[K]

Don't over-constrain. If a function works on any type, T is fine without a constraint.

Putting it all together

Here's a complete CLAUDE.md for a typical full-stack TypeScript project (Next.js frontend + Express API backend in a monorepo):

# Project: [Your Project Name]

## Overview
pnpm monorepo with a Next.js 14 App Router frontend and Express API backend.
TypeScript strict mode throughout.

packages/
  web/    # Next.js frontend (App Router, Tailwind)
  api/    # Express API server
  shared/ # Shared types and utilities

## TypeScript
Strict mode on in all packages. No any types.
Path aliases: @/* -> src/* (per package), @shared/* -> packages/shared/src/*
Module: ESM in web, CommonJS in api.

## Package Manager
pnpm 9. Never use npm or yarn.
  Install: pnpm add <pkg> --filter @project/web
  Run: pnpm --filter @project/web dev
  Run all: pnpm -r build

## Testing
Vitest for unit tests. Playwright for e2e.
  Unit: pnpm test (per package)
  E2E: pnpm test:e2e (from root)
Import from 'vitest': import { describe, it, expect, vi } from 'vitest';
Test files co-located: component.test.ts next to component.ts

## Linting
ESLint + Prettier.
  Lint: pnpm lint
  Format: pnpm format
No unused imports, no console.log in production code.

## Next.js (packages/web)
Server Components by default. 'use client' only when needed.
Data fetching in Server Components. Route handlers in app/api/.
Metadata exports for SEO.

## Express (packages/api)
Pattern: router -> controller -> service -> repository.
No business logic in controllers. No express imports in services.
Central error middleware handles all thrown errors.

## Guardrails
- Do not modify packages/shared types without checking all consumers
- Run pnpm typecheck before declaring a task done
- Ask before adding new package dependencies

Analyze your current config

If you already have a CLAUDE.md and want to know what's missing, ContextKit's free analyzer reviews your config against a TypeScript-specific checklist. It flags missing sections, vague rules, and common TypeScript gotchas that aren't covered.

Or if you're starting fresh, the ContextKit generator walks you through a wizard for TypeScript/Node.js projects - you pick your package manager, framework, test runner, and linter, and it generates a production-ready config with the rules above already included.

The generator also exports to .cursorrules and GEMINI.md if you work across multiple AI coding tools.

Want to build your own AI OS?

The AI OS Blueprint gives you the complete system: 53-page playbook, working skills, and a clonable repo. Starting at $47.

30-day money-back guarantee. No subscription.