typescript-project

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript Project Architecture

TypeScript项目架构

Core Principles

核心原则

  • Type safety first — Strict mode, no
    any
    , Zod for runtime validation
  • ESM native — ES Modules by default, Node 22+ / Bun
  • Layered architecture — Separate lib/services/adapters
  • 200-line limit — No file exceeds 200 lines (see elegant-architecture skill)
  • Test reality — Vitest/Bun test, minimal mocks
  • No backwards compatibility — Delete, don't deprecate. Change directly, no shims
  • LiteLLM for LLM APIs — Use LiteLLM proxy for all LLM integrations, unless specific SDK required

  • 类型安全优先 — 启用严格模式,禁止使用
    any
    类型,使用Zod进行运行时验证
  • 原生ESM — 默认使用ES模块,要求Node 22+ / Bun环境
  • 分层架构 — 分离lib/services/adapters目录
  • 200行限制 — 单个文件代码不超过200行(详见elegant-architecture skill)
  • 真实测试 — 使用Vitest/Bun测试,最小化Mock使用
  • 无向后兼容性 — 直接删除,不标记废弃。直接修改,不使用兼容垫片
  • LLM API使用LiteLLM — 所有LLM集成均通过LiteLLM代理,除非需要特定SDK

No Backwards Compatibility

无向后兼容性

Delete unused code. Change directly. No compatibility layers.
删除未使用代码。直接修改。不保留兼容层。

Why

原因

  • Dead code is tech debt
  • Compatibility shims add complexity
  • Old patterns spread through copy-paste
  • "Temporary" workarounds become permanent
  • 死代码是技术债务
  • 兼容垫片会增加复杂度
  • 旧模式会通过复制粘贴扩散
  • "临时"解决方案会变成永久遗留

Anti-Patterns to Avoid

需要避免的反模式

typescript
// ❌ BAD: Renaming but keeping old export
export { newName };
export { newName as oldName }; // "for backwards compatibility"

// ❌ BAD: Unused parameter with underscore
function process(_legacyParam: string, data: Data) { ... }

// ❌ BAD: Deprecated comments instead of deletion
/** @deprecated Use newMethod instead */
export function oldMethod() { ... }

// ❌ BAD: Re-exporting removed functionality
export { removed } from './legacy'; // Keep for existing consumers

// ❌ BAD: Feature flags for old behavior
if (config.useLegacyMode) { ... }
typescript
// ❌ BAD: Renaming but keeping old export
export { newName };
export { newName as oldName }; // "for backwards compatibility"

// ❌ BAD: Unused parameter with underscore
function process(_legacyParam: string, data: Data) { ... }

// ❌ BAD: Deprecated comments instead of deletion
/** @deprecated Use newMethod instead */
export function oldMethod() { ... }

// ❌ BAD: Re-exporting removed functionality
export { removed } from './legacy'; // Keep for existing consumers

// ❌ BAD: Feature flags for old behavior
if (config.useLegacyMode) { ... }

Correct Approach

正确做法

typescript
// ✅ GOOD: Just delete and update all usages
// Old: export { fetchData as getData }
// New: export { fetchData }
// Then: Find & replace all getData → fetchData

// ✅ GOOD: Remove unused parameters entirely
function process(data: Data) { ... }

// ✅ GOOD: Delete deprecated code, update callers
// Don't mark as deprecated, just remove it

// ✅ GOOD: Breaking changes are fine in active development
// Semantic versioning handles this for libraries
typescript
// ✅ GOOD: Just delete and update all usages
// Old: export { fetchData as getData }
// New: export { fetchData }
// Then: Find & replace all getData → fetchData

// ✅ GOOD: Remove unused parameters entirely
function process(data: Data) { ... }

// ✅ GOOD: Delete deprecated code, update callers
// Don't mark as deprecated, just remove it

// ✅ GOOD: Breaking changes are fine in active development
// Semantic versioning handles this for libraries

When Changing Interfaces

接口修改场景

typescript
// ❌ BAD: Adding optional fields "for compatibility"
interface User {
  id: string;
  name: string;
  firstName?: string; // New field, name kept for compatibility
  lastName?: string;
}

// ✅ GOOD: Clean break, update all usages
interface User {
  id: string;
  firstName: string;
  lastName: string;
}
// Then update ALL code that uses User.name
typescript
// ❌ BAD: Adding optional fields "for compatibility"
interface User {
  id: string;
  name: string;
  firstName?: string; // New field, name kept for compatibility
  lastName?: string;
}

// ✅ GOOD: Clean break, update all usages
interface User {
  id: string;
  firstName: string;
  lastName: string;
}
// Then update ALL code that uses User.name

Migration Strategy

迁移策略

  1. Find all usages
    grep -r "oldName" src/
  2. Update all at once — Single commit, no transition period
  3. Delete old code — No deprecation warnings, just remove
  4. Run tests — Ensure nothing breaks

  1. 查找所有引用
    grep -r "oldName" src/
  2. 一次性全部更新 — 单次提交,无过渡周期
  3. 删除旧代码 — 不保留废弃警告,直接移除
  4. 运行测试 — 确保无功能损坏

LiteLLM for LLM APIs

LLM API使用LiteLLM

Use LiteLLM proxy for all LLM integrations. Don't call provider APIs directly.
所有LLM集成均使用LiteLLM代理。不要直接调用供应商API。

Why LiteLLM

选择LiteLLM的原因

  • Unified interface — One API for 100+ LLM providers (OpenAI, Anthropic, Azure, Bedrock, etc.)
  • Provider agnostic — Switch models without code changes
  • Cost tracking — Built-in usage and cost monitoring
  • Load balancing — Automatic failover between providers
  • Rate limiting — Protect against quota exhaustion
  • 统一接口 — 一个API兼容100+ LLM供应商(OpenAI、Anthropic、Azure、Bedrock等)
  • 供应商无关 — 无需修改代码即可切换模型
  • 成本追踪 — 内置使用量和成本监控
  • 负载均衡 — 供应商间自动故障转移
  • 速率限制 — 防止配额耗尽

Setup

安装配置

bash
undefined
bash
undefined

Run LiteLLM proxy (Docker)

Run LiteLLM proxy (Docker)

docker run -p 4000:4000 ghcr.io/berriai/litellm:main-stable
docker run -p 4000:4000 ghcr.io/berriai/litellm:main-stable

Or install locally

Or install locally

pip install litellm[proxy] litellm --model gpt-4o
undefined
pip install litellm[proxy] litellm --model gpt-4o
undefined

TypeScript Usage

TypeScript使用示例

typescript
// adapters/llm.adapter.ts
import { OpenAI } from 'openai';

// Connect to LiteLLM proxy using OpenAI SDK
const llm = new OpenAI({
  baseURL: process.env.LITELLM_URL || 'http://localhost:4000',
  apiKey: process.env.LITELLM_API_KEY || 'sk-1234', // Proxy API key
});

export async function complete(prompt: string, model = 'gpt-4o'): Promise<string> {
  const response = await llm.chat.completions.create({
    model, // Can be any model: gpt-4o, claude-3-opus, gemini-pro, etc.
    messages: [{ role: 'user', content: prompt }],
  });
  return response.choices[0]?.message?.content ?? '';
}
typescript
// adapters/llm.adapter.ts
import { OpenAI } from 'openai';

// Connect to LiteLLM proxy using OpenAI SDK
const llm = new OpenAI({
  baseURL: process.env.LITELLM_URL || 'http://localhost:4000',
  apiKey: process.env.LITELLM_API_KEY || 'sk-1234', // Proxy API key
});

export async function complete(prompt: string, model = 'gpt-4o'): Promise<string> {
  const response = await llm.chat.completions.create({
    model, // Can be any model: gpt-4o, claude-3-opus, gemini-pro, etc.
    messages: [{ role: 'user', content: prompt }],
  });
  return response.choices[0]?.message?.content ?? '';
}

When NOT to Use LiteLLM

无需使用LiteLLM的场景

  • Streaming with provider-specific features (e.g., Anthropic's tool use streaming)
  • Provider-specific APIs not in OpenAI format (embeddings with metadata, etc.)
  • Direct SDK required for compliance/security reasons
  • 使用供应商特定流式功能(如Anthropic的工具调用流式)
  • 供应商特定API不兼容OpenAI格式(如带元数据的嵌入等)
  • 合规/安全要求必须使用直接SDK

Anti-Patterns

反模式

typescript
// ❌ BAD: Direct provider SDKs everywhere
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { GoogleGenerativeAI } from '@google/generative-ai';

// ❌ BAD: Provider-specific code scattered across codebase
if (provider === 'anthropic') { ... }
else if (provider === 'openai') { ... }

// ✅ GOOD: Single LiteLLM adapter, switch models via config
const response = await llm.chat.completions.create({
  model: config.llmModel, // "gpt-4o" or "claude-3-opus" or "gemini-pro"
  messages,
});

typescript
// ❌ BAD: Direct provider SDKs everywhere
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { GoogleGenerativeAI } from '@google/generative-ai';

// ❌ BAD: Provider-specific code scattered across codebase
if (provider === 'anthropic') { ... }
else if (provider === 'openai') { ... }

// ✅ GOOD: Single LiteLLM adapter, switch models via config
const response = await llm.chat.completions.create({
  model: config.llmModel, // "gpt-4o" or "claude-3-opus" or "gemini-pro"
  messages,
});

Quick Start

快速开始

1. Initialize Project

1. 初始化项目

bash
undefined
bash
undefined

Using Bun (recommended)

Using Bun (recommended)

bun init bun add zod bun add -d typescript @types/bun @biomejs/biome
bun init bun add zod bun add -d typescript @types/bun @biomejs/biome

Using Node.js

Using Node.js

npm init -y npm i zod npm i -D typescript @types/node tsx @biomejs/biome
undefined
npm init -y npm i zod npm i -D typescript @types/node tsx @biomejs/biome
undefined

2. Apply Tech Stack

2. 技术栈选型

LayerRecommendation
RuntimeBun / Node 22+
LanguageTypeScript (latest)
ValidationZod (latest)
TestingBun test / Vitest
Buildbun build / tsup
LintingBiome (latest)
层级推荐方案
运行时Bun / Node 22+
语言TypeScript (最新版)
验证Zod (最新版)
测试Bun test / Vitest
构建bun build / tsup
代码检查Biome (最新版)

Version Strategy

版本策略

Always use latest. Never pin versions in templates.
json
{
  "dependencies": {
    "zod": "latest"
  },
  "devDependencies": {
    "@biomejs/biome": "latest",
    "typescript": "latest"
  }
}
  • bun add
    /
    npm i
    automatically fetches latest
  • Use
    bun update --latest
    to upgrade all dependencies
  • Lock files (
    bun.lockb
    ,
    package-lock.json
    ) ensure reproducible builds
  • Breaking changes are handled by reading changelogs, not by avoiding updates
始终使用最新版。模板中绝不固定版本。
json
{
  "dependencies": {
    "zod": "latest"
  },
  "devDependencies": {
    "@biomejs/biome": "latest",
    "typescript": "latest"
  }
}
  • bun add
    /
    npm i
    会自动拉取最新版本
  • 使用
    bun update --latest
    升级所有依赖
  • 锁文件(
    bun.lockb
    ,
    package-lock.json
    )确保构建可复现
  • 通过阅读变更日志处理破坏性变更,而非避免更新

3. Use Standard Structure

3. 使用标准目录结构

project/
├── src/
│   ├── index.ts           # Entry point
│   ├── lib/               # Core utilities
│   │   ├── config.ts      # Configuration management
│   │   ├── errors.ts      # Custom error classes
│   │   ├── logger.ts      # Logging infrastructure
│   │   └── types.ts       # Shared type definitions
│   ├── services/          # Business logic
│   │   └── *.service.ts
│   └── adapters/          # External integrations
│       └── *.adapter.ts
├── tests/                 # Test files
│   └── *.test.ts
├── tsconfig.json
├── package.json
└── biome.json             # or eslint.config.js

project/
├── src/
│   ├── index.ts           # 入口文件
│   ├── lib/               # 核心工具库
│   │   ├── config.ts      # 配置管理
│   │   ├── errors.ts      # 自定义错误类
│   │   ├── logger.ts      # 日志基础设施
│   │   └── types.ts       # 共享类型定义
│   ├── services/          # 业务逻辑
│   │   └── *.service.ts
│   └── adapters/          # 外部集成
│       └── *.adapter.ts
├── tests/                 # 测试文件
│   └── *.test.ts
├── tsconfig.json
├── package.json
└── biome.json             # 或 eslint.config.js

Architecture Layers

架构分层

lib/ — Core Infrastructure

lib/ — 核心基础设施

Foundational code used across the entire application:
typescript
// lib/types.ts — Shared type definitions
export interface Result<T, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
}

// lib/errors.ts — Custom errors
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// lib/config.ts — Configuration
export const config = {
  env: process.env.NODE_ENV || 'development',
  port: Number(process.env.PORT) || 3000,
  db: {
    url: process.env.DATABASE_URL!,
  },
} as const;

// lib/logger.ts — Logging (see structured-logging skill)
全应用共享的基础代码:
typescript
// lib/types.ts — Shared type definitions
export interface Result<T, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
}

// lib/errors.ts — Custom errors
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// lib/config.ts — Configuration
export const config = {
  env: process.env.NODE_ENV || 'development',
  port: Number(process.env.PORT) || 3000,
  db: {
    url: process.env.DATABASE_URL!,
  },
} as const;

// lib/logger.ts — Logging (see structured-logging skill)

services/ — Business Logic

services/ — 业务逻辑

Pure business logic with injected dependencies:
typescript
// services/user.service.ts
export class UserService {
  constructor(private readonly userRepo: UserRepository) {}

  async create(input: CreateUserInput): Promise<User> {
    const existing = await this.userRepo.findByEmail(input.email);
    if (existing) throw new AppError('Email exists', 'USER_EXISTS', 409);
    return this.userRepo.save(User.create(input));
  }
}
纯业务逻辑,依赖注入:
typescript
// services/user.service.ts
export class UserService {
  constructor(private readonly userRepo: UserRepository) {}

  async create(input: CreateUserInput): Promise<User> {
    const existing = await this.userRepo.findByEmail(input.email);
    if (existing) throw new AppError('Email exists', 'USER_EXISTS', 409);
    return this.userRepo.save(User.create(input));
  }
}

adapters/ — External Integrations

adapters/ — 外部集成

Interface with external systems (DB, APIs, file system):
typescript
// adapters/postgres.adapter.ts
export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: Database) {}

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
    return row ? User.fromRow(row) : null;
  }
}

与外部系统交互(数据库、API、文件系统):
typescript
// adapters/postgres.adapter.ts
export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: Database) {}

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
    return row ? User.fromRow(row) : null;
  }
}

Configuration Files

配置文件

tsconfig.json (2025)

tsconfig.json (2025)

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

package.json

package.json

json
{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist --target bun",
    "start": "bun dist/index.js",
    "test": "bun test",
    "typecheck": "tsc --noEmit"
  }
}

json
{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist --target bun",
    "start": "bun dist/index.js",
    "test": "bun test",
    "typecheck": "tsc --noEmit"
  }
}

Validation with Zod

Zod验证

typescript
import { z } from 'zod';

// Define schemas
export const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
});

// Infer types from schemas
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

// Validate at boundaries
export function validateInput<T>(schema: z.ZodType<T>, data: unknown): T {
  return schema.parse(data);
}

typescript
import { z } from 'zod';

// Define schemas
export const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
});

// Infer types from schemas
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

// Validate at boundaries
export function validateInput<T>(schema: z.ZodType<T>, data: unknown): T {
  return schema.parse(data);
}

Error Handling Pattern

错误处理模式

typescript
// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }

  static notFound(resource: string, id: string) {
    return new AppError(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }

  static validation(message: string, context?: Record<string, unknown>) {
    return new AppError(message, 'VALIDATION_ERROR', 400, context);
  }
}

// Usage
throw AppError.notFound('User', userId);

typescript
// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }

  static notFound(resource: string, id: string) {
    return new AppError(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }

  static validation(message: string, context?: Record<string, unknown>) {
    return new AppError(message, 'VALIDATION_ERROR', 400, context);
  }
}

// Usage
throw AppError.notFound('User', userId);

Testing Strategy

测试策略

typescript
// tests/user.service.test.ts
import { describe, it, expect, beforeEach } from 'bun:test';
import { UserService } from '../src/services/user.service';
import { InMemoryUserRepository } from './helpers/in-memory-repo';

describe('UserService', () => {
  let service: UserService;
  let repo: InMemoryUserRepository;

  beforeEach(() => {
    repo = new InMemoryUserRepository();
    service = new UserService(repo);
  });

  it('creates user with valid input', async () => {
    const user = await service.create({
      email: 'test@example.com',
      name: 'Test User',
    });

    expect(user.email).toBe('test@example.com');
    expect(await repo.findByEmail('test@example.com')).toEqual(user);
  });

  it('rejects duplicate email', async () => {
    await service.create({ email: 'test@example.com', name: 'User 1' });

    expect(
      service.create({ email: 'test@example.com', name: 'User 2' })
    ).rejects.toThrow('Email exists');
  });
});

typescript
// tests/user.service.test.ts
import { describe, it, expect, beforeEach } from 'bun:test';
import { UserService } from '../src/services/user.service';
import { InMemoryUserRepository } from './helpers/in-memory-repo';

describe('UserService', () => {
  let service: UserService;
  let repo: InMemoryUserRepository;

  beforeEach(() => {
    repo = new InMemoryUserRepository();
    service = new UserService(repo);
  });

  it('creates user with valid input', async () => {
    const user = await service.create({
      email: 'test@example.com',
      name: 'Test User',
    });

    expect(user.email).toBe('test@example.com');
    expect(await repo.findByEmail('test@example.com')).toEqual(user);
  });

  it('rejects duplicate email', async () => {
    await service.create({ email: 'test@example.com', name: 'User 1' });

    expect(
      service.create({ email: 'test@example.com', name: 'User 2' })
    ).rejects.toThrow('Email exists');
  });
});

Checklist

检查清单

markdown
undefined
markdown
undefined

Project Setup

项目设置

  • TypeScript strict mode enabled
  • ESM modules configured
  • Biome/ESLint configured
  • Testing framework ready
  • 启用TypeScript严格模式
  • 配置ESM模块
  • 配置Biome/ESLint
  • 准备好测试框架

Architecture

架构

  • lib/ for core utilities
  • services/ for business logic
  • adapters/ for external integrations
  • Clear module boundaries
  • 使用lib/存放核心工具
  • 使用services/存放业务逻辑
  • 使用adapters/存放外部集成
  • 模块边界清晰

Quality

质量

  • Zod schemas for validation
  • Custom error classes
  • Structured logging
  • Tests for critical paths
  • 使用Zod schema进行验证
  • 自定义错误类
  • 结构化日志
  • 关键路径有测试覆盖

Build

构建

  • Build script configured
  • Type checking in CI
  • Tests in CI

---
  • 配置构建脚本
  • CI中加入类型检查
  • CI中加入测试环节

---

See Also

扩展阅读

  • reference/architecture.md — Detailed architecture patterns
  • reference/tech-stack.md — Tech stack comparison
  • reference/patterns.md — Design patterns
  • elegant-architecture skill — 200-line file limit
  • structured-logging skill — Logging setup
  • reference/architecture.md — 详细架构模式
  • reference/tech-stack.md — 技术栈对比
  • reference/patterns.md — 设计模式
  • elegant-architecture skill — 200行文件限制
  • structured-logging skill — 日志设置