TypeScript + Node.js — Cline Rules
Cline rules for autonomous TypeScript + Node workflows with explicit safety boundaries.
Install path
Use this file for each supported tool in your project.
- Cline: Save as
.clinerulesin your project at.clinerules.
Configuration
.clinerules
1# TypeScript + Node.js — Cline Rules23TypeScript backend service running on Node.js. Strict mode, explicit error handling, fully tested. You operate autonomously — follow the workflow below and respect the safety boundaries. When in doubt, ask.45## Quick Reference67| Area | Convention |8|---|---|9| Language | TypeScript 5.x, strict mode |10| Runtime | Node.js 20+ (ESM — `"type": "module"`) |11| Package manager | Check lockfile: `pnpm-lock.yaml` → pnpm, `package-lock.json` → npm |12| Testing | Vitest (`pnpm test`) |13| Linting | ESLint + `@typescript-eslint` (`pnpm lint`) |14| Formatting | Prettier (`pnpm format`) |15| Build | `pnpm build` (tsc) |16| Type check | `pnpm typecheck` (tsc --noEmit) |1718## Autonomous Workflow1920Follow this sequence for every task. Do not skip steps.2122### 1. Scan — Understand Before Acting2324```25Read the relevant files before writing anything:26- If modifying a module → read it and its tests27- If adding a feature → read adjacent modules for patterns28- If fixing a bug → read the failing test and the module under test29- Always check package.json for existing deps before suggesting new ones30```3132### 2. Plan — Think Before Coding3334Before writing code, state your plan:35- What files you'll create or modify36- What patterns you'll follow (based on existing code)37- What tests you'll write3839### 3. Execute — Write Code4041Follow the code conventions below. Write implementation and tests together.4243### 4. Verify — Confirm It Works4445After every change, run the verification commands:4647```bash48pnpm typecheck # TypeScript compilation49pnpm lint # ESLint50pnpm test # Vitest (full suite)51```5253**Do not consider a task complete until all three pass.** If they fail, fix the issues and re-run.5455## Safety Boundaries5657### ✅ Do Independently (No Approval Needed)5859- Read any file in the project60- Create new source files following existing patterns61- Write or update tests62- Fix lint errors and type errors63- Add error handling to existing code64- Refactor within a single module (rename variables, extract functions)65- Run tests, lint, and type-check commands66- Update existing documentation to match code changes6768### ⚠️ Ask First (Get Approval Before Proceeding)6970- **Adding dependencies** — state the package name, why it's needed, and what alternative you considered71- **Deleting files** — explain what's being removed and why72- **Modifying configuration files** — `tsconfig.json`, `package.json` scripts, `.eslintrc`, CI config73- **Changing the public API** — function signatures, exported types, endpoint contracts74- **Database schema changes** — migrations, model changes75- **Modifying auth/security logic** — anything touching authentication, authorization, or secrets76- **Refactoring across multiple modules** — changes spanning 3+ files that alter interfaces77- **Installing system-level tools** — anything via `apt`, `brew`, `npm install -g`7879### 🚫 Never Do8081- Run `rm -rf` or delete directories without explicit approval82- Commit or push to git83- Modify `.env` files or hardcode secrets84- Run production scripts or deployment commands85- Install global packages86- Modify code outside the project directory8788## Project Structure8990```91├── src/92│ ├── index.ts # Entry point — wires dependencies, starts server93│ ├── config.ts # Environment parsing (zod + process.env)94│ ├── errors.ts # Custom error classes95│ ├── types.ts # Shared TypeScript types96│ ├── handlers/ # HTTP request handlers (thin — call services)97│ │ ├── user.handler.ts98│ │ └── order.handler.ts99│ ├── services/ # Business logic100│ │ ├── user.service.ts101│ │ └── order.service.ts102│ ├── repositories/ # Data access103│ │ ├── user.repository.ts104│ │ └── order.repository.ts105│ ├── middleware/ # HTTP middleware (auth, logging, error handling)106│ └── utils/ # Pure utility functions107├── tests/108│ ├── helpers/ # Test utilities and factories109│ │ └── factories.ts110│ ├── unit/111│ │ ├── services/112│ │ └── utils/113│ └── integration/114│ └── handlers/115├── tsconfig.json116├── vitest.config.ts117├── package.json118└── Dockerfile119```120121**Dependency direction:** handler → service → repository. Services never import HTTP types. Repositories never throw HTTP errors.122123## Error Handling124125### Custom Error Hierarchy126127```typescript128// src/errors.ts129export class AppError extends Error {130 constructor(131 message: string,132 public readonly code: string,133 public readonly statusCode: number = 500,134 public readonly cause?: Error135 ) {136 super(message);137 this.name = this.constructor.name;138 }139}140141export class NotFoundError extends AppError {142 constructor(resource: string, id: string) {143 super(`${resource} '${id}' not found`, "NOT_FOUND", 404);144 }145}146147export class ValidationError extends AppError {148 constructor(149 message: string,150 public readonly fields: Record<string, string>151 ) {152 super(message, "VALIDATION_ERROR", 400);153 }154}155156export class ConflictError extends AppError {157 constructor(message: string) {158 super(message, "CONFLICT", 409);159 }160}161162export class UnauthorizedError extends AppError {163 constructor(message: string = "Unauthorized") {164 super(message, "UNAUTHORIZED", 401);165 }166}167```168169### Use These Errors Everywhere — No Raw `throw new Error()`170171```typescript172// ✅ Service throws domain errors173export class OrderService {174 constructor(175 private readonly repo: OrderRepository,176 private readonly logger: Logger177 ) {}178179 async getById(id: string): Promise<Order> {180 const order = await this.repo.findById(id);181 if (!order) {182 throw new NotFoundError("Order", id);183 }184 return order;185 }186187 async cancel(id: string): Promise<Order> {188 const order = await this.getById(id);189 if (order.status !== "pending") {190 throw new ConflictError(191 `Cannot cancel order in '${order.status}' status`192 );193 }194 return this.repo.updateStatus(id, "cancelled");195 }196}197```198199### Central Error Handler Middleware200201```typescript202// src/middleware/error-handler.ts203import { AppError } from "../errors.js";204205export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {206 if (err instanceof AppError) {207 res.status(err.statusCode).json({208 error: { code: err.code, message: err.message },209 });210 return;211 }212213 // Unknown error — log full details, return generic message214 logger.error("Unhandled error", { error: err.message, stack: err.stack });215 res.status(500).json({216 error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" },217 });218}219```220221## Validation222223### Zod at API Boundaries224225```typescript226// src/handlers/order.handler.ts227import { z } from "zod";228229const createOrderSchema = z.object({230 customerId: z.string().min(1),231 items: z.array(z.object({232 productId: z.string().min(1),233 quantity: z.number().int().positive(),234 unitPrice: z.number().positive(),235 })).min(1),236 notes: z.string().max(500).optional(),237});238239export async function createOrder(req: Request, res: Response, next: NextFunction) {240 const parsed = createOrderSchema.safeParse(req.body);241 if (!parsed.success) {242 const fieldErrors = Object.fromEntries(243 parsed.error.issues.map((i) => [i.path.join("."), i.message])244 );245 throw new ValidationError("Invalid input", fieldErrors);246 }247248 const order = await orderService.create(parsed.data);249 res.status(201).json(order);250}251```252253### Config Validation at Startup254255```typescript256// src/config.ts257import { z } from "zod";258259const envSchema = z.object({260 NODE_ENV: z.enum(["development", "production", "test"]).default("development"),261 PORT: z.coerce.number().int().positive().default(3000),262 DATABASE_URL: z.string().url(),263 REDIS_URL: z.string().url().optional(),264 LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),265 API_KEY: z.string().min(1),266});267268export type Config = z.infer<typeof envSchema>;269270export function loadConfig(): Config {271 const result = envSchema.safeParse(process.env);272 if (!result.success) {273 console.error("Invalid environment configuration:");274 console.error(result.error.flatten().fieldErrors);275 process.exit(1);276 }277 return result.data;278}279```280281## Testing282283### Unit Tests — Services284285```typescript286// tests/unit/services/order.service.test.ts287import { describe, it, expect, vi, beforeEach } from "vitest";288import { OrderService } from "../../../src/services/order.service.js";289import { NotFoundError, ConflictError } from "../../../src/errors.js";290291describe("OrderService", () => {292 let service: OrderService;293 let mockRepo: { findById: ReturnType<typeof vi.fn>; updateStatus: ReturnType<typeof vi.fn> };294295 beforeEach(() => {296 mockRepo = {297 findById: vi.fn(),298 updateStatus: vi.fn(),299 insert: vi.fn(),300 };301 service = new OrderService(mockRepo, mockLogger);302 });303304 describe("getById", () => {305 it("returns the order when found", async () => {306 const order = { id: "ord_1", status: "pending", total: 100 };307 mockRepo.findById.mockResolvedValue(order);308309 const result = await service.getById("ord_1");310311 expect(result).toEqual(order);312 expect(mockRepo.findById).toHaveBeenCalledWith("ord_1");313 });314315 it("throws NotFoundError when order does not exist", async () => {316 mockRepo.findById.mockResolvedValue(null);317318 await expect(service.getById("ord_999")).rejects.toThrow(NotFoundError);319 });320 });321322 describe("cancel", () => {323 it("cancels a pending order", async () => {324 const order = { id: "ord_1", status: "pending" };325 mockRepo.findById.mockResolvedValue(order);326 mockRepo.updateStatus.mockResolvedValue({ ...order, status: "cancelled" });327328 const result = await service.cancel("ord_1");329330 expect(result.status).toBe("cancelled");331 expect(mockRepo.updateStatus).toHaveBeenCalledWith("ord_1", "cancelled");332 });333334 it("throws ConflictError when order is already shipped", async () => {335 mockRepo.findById.mockResolvedValue({ id: "ord_1", status: "shipped" });336337 await expect(service.cancel("ord_1")).rejects.toThrow(ConflictError);338 expect(mockRepo.updateStatus).not.toHaveBeenCalled();339 });340 });341});342```343344### Integration Tests — Use `supertest` Against the App345346```typescript347// tests/integration/handlers/order.handler.test.ts348it("creates an order with valid input", async () => {349 const response = await request350 .post("/api/orders")351 .send({352 customerId: "cust_1",353 items: [{ productId: "prod_1", quantity: 2, unitPrice: 29.99 }],354 })355 .expect(201);356357 expect(response.body).toMatchObject({ customerId: "cust_1", status: "pending" });358});359360it("returns 400 for invalid input", async () => {361 await request.post("/api/orders").send({ customerId: "", items: [] }).expect(400);362});363```364365## Common Patterns366367### Dependency Injection via Constructor368369```typescript370// Accept interfaces, construct in entry point371interface UserRepository {372 findById(id: string): Promise<User | undefined>;373 insert(data: CreateUserData): Promise<User>;374}375376class UserService {377 constructor(378 private readonly repo: UserRepository,379 private readonly logger: Logger380 ) {}381}382383// src/index.ts — wire it up384const userRepo = new PostgresUserRepository(pool);385const userService = new UserService(userRepo, logger);386const userHandler = new UserHandler(userService);387```388389### Structured Logging390391```typescript392import pino from "pino";393394const logger = pino({395 level: config.LOG_LEVEL,396 transport: config.NODE_ENV === "development"397 ? { target: "pino-pretty" }398 : undefined,399});400401// Always log with context402logger.info({ orderId, customerId }, "Order created");403logger.error({ error: err.message, orderId }, "Failed to process order");404405// Never log: passwords, tokens, API keys, PII, full request bodies406```407408## Import Conventions409410```typescript411// 1. Node.js built-ins (with node: prefix)412import { readFile } from "node:fs/promises";413import { join } from "node:path";414415// 2. External packages416import { z } from "zod";417import pino from "pino";418419// 3. Internal modules420import { AppError } from "./errors.js";421import { UserService } from "./services/user.service.js";422import type { User } from "./types.js";423424// ✅ Use .js extension in import paths (ESM requires it)425// ✅ Use `import type` for type-only imports426```427428## Verification Checklist429430Before reporting a task as complete, confirm:431432- [ ] `pnpm typecheck` passes (no TypeScript errors)433- [ ] `pnpm lint` passes (no ESLint errors)434- [ ] `pnpm test` passes (all tests green)435- [ ] New code has tests (unit tests at minimum)436- [ ] Error cases are handled (not just the happy path)437- [ ] No `any` types introduced438- [ ] No `console.log` — use the structured logger439- [ ] Imports use `.js` extension and `import type` where appropriate440
Community feedback
0 found this helpful
Works with: