dotmd

TypeScript + Node.js — Cline Rules

Cline rules for autonomous TypeScript + Node workflows with explicit safety boundaries.

By dotmd TeamCC0Published Feb 19, 2026View source ↗

Install path

Use this file for each supported tool in your project.

  • Cline: Save as .clinerules in your project at .clinerules.

Configuration

.clinerules

1# TypeScript + Node.js — Cline Rules
2
3TypeScript 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.
4
5## Quick Reference
6
7| 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) |
17
18## Autonomous Workflow
19
20Follow this sequence for every task. Do not skip steps.
21
22### 1. Scan — Understand Before Acting
23
24```
25Read the relevant files before writing anything:
26- If modifying a module → read it and its tests
27- If adding a feature → read adjacent modules for patterns
28- If fixing a bug → read the failing test and the module under test
29- Always check package.json for existing deps before suggesting new ones
30```
31
32### 2. Plan — Think Before Coding
33
34Before writing code, state your plan:
35- What files you'll create or modify
36- What patterns you'll follow (based on existing code)
37- What tests you'll write
38
39### 3. Execute — Write Code
40
41Follow the code conventions below. Write implementation and tests together.
42
43### 4. Verify — Confirm It Works
44
45After every change, run the verification commands:
46
47```bash
48pnpm typecheck # TypeScript compilation
49pnpm lint # ESLint
50pnpm test # Vitest (full suite)
51```
52
53**Do not consider a task complete until all three pass.** If they fail, fix the issues and re-run.
54
55## Safety Boundaries
56
57### ✅ Do Independently (No Approval Needed)
58
59- Read any file in the project
60- Create new source files following existing patterns
61- Write or update tests
62- Fix lint errors and type errors
63- Add error handling to existing code
64- Refactor within a single module (rename variables, extract functions)
65- Run tests, lint, and type-check commands
66- Update existing documentation to match code changes
67
68### ⚠️ Ask First (Get Approval Before Proceeding)
69
70- **Adding dependencies** — state the package name, why it's needed, and what alternative you considered
71- **Deleting files** — explain what's being removed and why
72- **Modifying configuration files** — `tsconfig.json`, `package.json` scripts, `.eslintrc`, CI config
73- **Changing the public API** — function signatures, exported types, endpoint contracts
74- **Database schema changes** — migrations, model changes
75- **Modifying auth/security logic** — anything touching authentication, authorization, or secrets
76- **Refactoring across multiple modules** — changes spanning 3+ files that alter interfaces
77- **Installing system-level tools** — anything via `apt`, `brew`, `npm install -g`
78
79### 🚫 Never Do
80
81- Run `rm -rf` or delete directories without explicit approval
82- Commit or push to git
83- Modify `.env` files or hardcode secrets
84- Run production scripts or deployment commands
85- Install global packages
86- Modify code outside the project directory
87
88## Project Structure
89
90```
91├── src/
92│ ├── index.ts # Entry point — wires dependencies, starts server
93│ ├── config.ts # Environment parsing (zod + process.env)
94│ ├── errors.ts # Custom error classes
95│ ├── types.ts # Shared TypeScript types
96│ ├── handlers/ # HTTP request handlers (thin — call services)
97│ │ ├── user.handler.ts
98│ │ └── order.handler.ts
99│ ├── services/ # Business logic
100│ │ ├── user.service.ts
101│ │ └── order.service.ts
102│ ├── repositories/ # Data access
103│ │ ├── user.repository.ts
104│ │ └── order.repository.ts
105│ ├── middleware/ # HTTP middleware (auth, logging, error handling)
106│ └── utils/ # Pure utility functions
107├── tests/
108│ ├── helpers/ # Test utilities and factories
109│ │ └── factories.ts
110│ ├── unit/
111│ │ ├── services/
112│ │ └── utils/
113│ └── integration/
114│ └── handlers/
115├── tsconfig.json
116├── vitest.config.ts
117├── package.json
118└── Dockerfile
119```
120
121**Dependency direction:** handler → service → repository. Services never import HTTP types. Repositories never throw HTTP errors.
122
123## Error Handling
124
125### Custom Error Hierarchy
126
127```typescript
128// src/errors.ts
129export class AppError extends Error {
130 constructor(
131 message: string,
132 public readonly code: string,
133 public readonly statusCode: number = 500,
134 public readonly cause?: Error
135 ) {
136 super(message);
137 this.name = this.constructor.name;
138 }
139}
140
141export class NotFoundError extends AppError {
142 constructor(resource: string, id: string) {
143 super(`${resource} '${id}' not found`, "NOT_FOUND", 404);
144 }
145}
146
147export 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}
155
156export class ConflictError extends AppError {
157 constructor(message: string) {
158 super(message, "CONFLICT", 409);
159 }
160}
161
162export class UnauthorizedError extends AppError {
163 constructor(message: string = "Unauthorized") {
164 super(message, "UNAUTHORIZED", 401);
165 }
166}
167```
168
169### Use These Errors Everywhere — No Raw `throw new Error()`
170
171```typescript
172// ✅ Service throws domain errors
173export class OrderService {
174 constructor(
175 private readonly repo: OrderRepository,
176 private readonly logger: Logger
177 ) {}
178
179 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 }
186
187 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```
198
199### Central Error Handler Middleware
200
201```typescript
202// src/middleware/error-handler.ts
203import { AppError } from "../errors.js";
204
205export 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 }
212
213 // Unknown error — log full details, return generic message
214 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```
220
221## Validation
222
223### Zod at API Boundaries
224
225```typescript
226// src/handlers/order.handler.ts
227import { z } from "zod";
228
229const 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});
238
239export 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 }
247
248 const order = await orderService.create(parsed.data);
249 res.status(201).json(order);
250}
251```
252
253### Config Validation at Startup
254
255```typescript
256// src/config.ts
257import { z } from "zod";
258
259const 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});
267
268export type Config = z.infer<typeof envSchema>;
269
270export 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```
280
281## Testing
282
283### Unit Tests — Services
284
285```typescript
286// tests/unit/services/order.service.test.ts
287import { describe, it, expect, vi, beforeEach } from "vitest";
288import { OrderService } from "../../../src/services/order.service.js";
289import { NotFoundError, ConflictError } from "../../../src/errors.js";
290
291describe("OrderService", () => {
292 let service: OrderService;
293 let mockRepo: { findById: ReturnType<typeof vi.fn>; updateStatus: ReturnType<typeof vi.fn> };
294
295 beforeEach(() => {
296 mockRepo = {
297 findById: vi.fn(),
298 updateStatus: vi.fn(),
299 insert: vi.fn(),
300 };
301 service = new OrderService(mockRepo, mockLogger);
302 });
303
304 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);
308
309 const result = await service.getById("ord_1");
310
311 expect(result).toEqual(order);
312 expect(mockRepo.findById).toHaveBeenCalledWith("ord_1");
313 });
314
315 it("throws NotFoundError when order does not exist", async () => {
316 mockRepo.findById.mockResolvedValue(null);
317
318 await expect(service.getById("ord_999")).rejects.toThrow(NotFoundError);
319 });
320 });
321
322 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" });
327
328 const result = await service.cancel("ord_1");
329
330 expect(result.status).toBe("cancelled");
331 expect(mockRepo.updateStatus).toHaveBeenCalledWith("ord_1", "cancelled");
332 });
333
334 it("throws ConflictError when order is already shipped", async () => {
335 mockRepo.findById.mockResolvedValue({ id: "ord_1", status: "shipped" });
336
337 await expect(service.cancel("ord_1")).rejects.toThrow(ConflictError);
338 expect(mockRepo.updateStatus).not.toHaveBeenCalled();
339 });
340 });
341});
342```
343
344### Integration Tests — Use `supertest` Against the App
345
346```typescript
347// tests/integration/handlers/order.handler.test.ts
348it("creates an order with valid input", async () => {
349 const response = await request
350 .post("/api/orders")
351 .send({
352 customerId: "cust_1",
353 items: [{ productId: "prod_1", quantity: 2, unitPrice: 29.99 }],
354 })
355 .expect(201);
356
357 expect(response.body).toMatchObject({ customerId: "cust_1", status: "pending" });
358});
359
360it("returns 400 for invalid input", async () => {
361 await request.post("/api/orders").send({ customerId: "", items: [] }).expect(400);
362});
363```
364
365## Common Patterns
366
367### Dependency Injection via Constructor
368
369```typescript
370// Accept interfaces, construct in entry point
371interface UserRepository {
372 findById(id: string): Promise<User | undefined>;
373 insert(data: CreateUserData): Promise<User>;
374}
375
376class UserService {
377 constructor(
378 private readonly repo: UserRepository,
379 private readonly logger: Logger
380 ) {}
381}
382
383// src/index.ts — wire it up
384const userRepo = new PostgresUserRepository(pool);
385const userService = new UserService(userRepo, logger);
386const userHandler = new UserHandler(userService);
387```
388
389### Structured Logging
390
391```typescript
392import pino from "pino";
393
394const logger = pino({
395 level: config.LOG_LEVEL,
396 transport: config.NODE_ENV === "development"
397 ? { target: "pino-pretty" }
398 : undefined,
399});
400
401// Always log with context
402logger.info({ orderId, customerId }, "Order created");
403logger.error({ error: err.message, orderId }, "Failed to process order");
404
405// Never log: passwords, tokens, API keys, PII, full request bodies
406```
407
408## Import Conventions
409
410```typescript
411// 1. Node.js built-ins (with node: prefix)
412import { readFile } from "node:fs/promises";
413import { join } from "node:path";
414
415// 2. External packages
416import { z } from "zod";
417import pino from "pino";
418
419// 3. Internal modules
420import { AppError } from "./errors.js";
421import { UserService } from "./services/user.service.js";
422import type { User } from "./types.js";
423
424// ✅ Use .js extension in import paths (ESM requires it)
425// ✅ Use `import type` for type-only imports
426```
427
428## Verification Checklist
429
430Before reporting a task as complete, confirm:
431
432- [ ] `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 introduced
438- [ ] No `console.log` — use the structured logger
439- [ ] Imports use `.js` extension and `import type` where appropriate
440

Community feedback

0 found this helpful

Works with: