TypeScript & Node.js Development
Description
TypeScript and Node.js coding guide — covers strict TypeScript configuration, async patterns, error handling with Zod, Node.js APIs, testing with Vitest, and project structure conventions.
Triggers
- typescript
- javascript
- node
- nodejs
- npm
- pnpm
- express
- zod
- vitest
- jest
- ts
- js
Instructions
1. Before Writing Code
- Read the project's
tsconfig.jsonandpackage.jsonto understand config and dependencies. - Check existing patterns — naming conventions, module structure, export style.
- For non-trivial features, outline the approach before implementing.
- Identify edge cases: null/undefined inputs, network failures, type mismatches.
2. TypeScript Style
- Strict mode: always
"strict": truein tsconfig constby default,letonly when mutation is needed, nevervarinterfacefor object shapes,typefor unions/intersections/mapped typesasync/awaitover raw Promises or callbacks- Named exports over default exports (except Next.js pages/layouts)
- Explicit return types on exported functions
- Avoid
any— useunknownand narrow with type guards
// tsconfig.json essentials
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}
3. Error Handling
// Result pattern with typed errors
type Result<T> = { success: true; data: T } | { success: false; error: string };
async function fetchData(url: string): Promise<Result<Data>> {
try {
const response = await fetch(url);
if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` };
}
const data = await response.json();
return { success: true, data };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return { success: false, error: message };
}
}
Input Validation with Zod
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
type User = z.infer<typeof UserSchema>;
function createUser(input: unknown): User {
return UserSchema.parse(input); // throws ZodError on invalid input
}
// Or safe parsing (no throw)
const result = UserSchema.safeParse(input);
if (!result.success) {
console.error(result.error.flatten());
return;
}
const user = result.data; // fully typed
4. Node.js Patterns
// Use node: prefix for built-in modules
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { existsSync } from "node:fs";
// Async file operations (never use sync in production)
const content = await readFile(filePath, "utf-8");
await mkdir(dirPath, { recursive: true });
await writeFile(outputPath, data, "utf-8");
Timeouts with AbortController
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
Subprocess Execution
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const { stdout, stderr } = await execFileAsync("git", ["status"], {
cwd: projectRoot,
timeout: 10_000,
});
Environment Variables
// Type-safe env access
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) throw new Error(`Missing env var: ${key}`);
return value;
}
const port = parseInt(process.env.PORT ?? "3000", 10);
5. Project Structure
project/
src/
index.ts # entry point
types.ts # shared type definitions
utils/ # utility functions
services/ # business logic
test/
*.test.ts # test files mirror src/ structure
package.json
tsconfig.json
.env.example
Conventions:
- One module per file, named after the primary export
- Barrel exports (
index.ts) only at package boundaries, not everywhere - Keep
types.tsseparate from implementation - Co-locate tests next to source or in a parallel
test/directory
6. Testing
// Vitest (recommended — fast, ESM-native, Jest-compatible API)
import { describe, it, expect, vi } from "vitest";
describe("fetchData", () => {
it("returns data on success", async () => {
const result = await fetchData("https://api.example.com/data");
expect(result.success).toBe(true);
});
it("handles network errors", async () => {
const result = await fetchData("https://invalid.example.com");
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
it("mocks external dependencies", () => {
const mockFn = vi.fn().mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn).toHaveBeenCalledOnce();
});
});
Run: npx vitest (watch mode) or npx vitest run (single pass)
7. Code Review Checklist
- Types: No
any, strict mode passes, return types on exports - Null safety:
noUncheckedIndexedAccessenabled, null checks before access - Error handling: All async operations in try-catch, errors typed not swallowed
- Resources: Streams/connections closed, AbortControllers cleaned up
- Security: Input validated (Zod), no eval(), no unescaped user input in HTML
- Dependencies: Minimal, well-maintained packages; lockfile committed
- Tests: Coverage for happy path and error cases
8. Common Pitfalls
- Forgetting
await— an unhandled promise silently fails - Using
==instead of===— TypeScript catches some but not all - Mutating function arguments — always return new objects/arrays
- Circular imports — restructure to break the cycle, or use dynamic imports
- Large bundle size — check with
npx bundlesizeor build analyzer - Sync I/O in async context — use
fs/promises, notfs.readFileSync
Verify
- The code was actually executed (or type-checked / linted as appropriate) and the command output is captured
- Dependencies and runtime versions used are pinned and recorded (e.g., requirements.txt, package.json + lockfile, .nvmrc)
- Errors or warnings emitted by the run are addressed or explicitly accepted with a reason
- New external I/O (network, filesystem, DB) has timeouts and error handling, not silent failure
- Tests for the change were run and the pass/fail count is in the transcript
- Secrets and credentials are read from env/secret store, not hard-coded, and
.envfiles are not committed
Notes
For Next.js/React-specific patterns, read the react-best-practices and
composition-patterns skills instead — this skill covers pure TypeScript
and Node.js runtime patterns.