typescript-project
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTypeScript Project Architecture
TypeScript项目架构
Core Principles
核心原则
- Type safety first — Strict mode, no , Zod for runtime validation
any - 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
- 类型安全优先 — 启用严格模式,禁止使用类型,使用Zod进行运行时验证
any - 原生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 librariestypescript
// ✅ 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 librariesWhen 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.nametypescript
// ❌ 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.nameMigration Strategy
迁移策略
- Find all usages —
grep -r "oldName" src/ - Update all at once — Single commit, no transition period
- Delete old code — No deprecation warnings, just remove
- Run tests — Ensure nothing breaks
- 查找所有引用 —
grep -r "oldName" src/ - 一次性全部更新 — 单次提交,无过渡周期
- 删除旧代码 — 不保留废弃警告,直接移除
- 运行测试 — 确保无功能损坏
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
undefinedbash
undefinedRun 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
undefinedpip install litellm[proxy]
litellm --model gpt-4o
undefinedTypeScript 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
undefinedbash
undefinedUsing 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
undefinednpm init -y
npm i zod
npm i -D typescript @types/node tsx @biomejs/biome
undefined2. Apply Tech Stack
2. 技术栈选型
| Layer | Recommendation |
|---|---|
| Runtime | Bun / Node 22+ |
| Language | TypeScript (latest) |
| Validation | Zod (latest) |
| Testing | Bun test / Vitest |
| Build | bun build / tsup |
| Linting | Biome (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 addautomatically fetches latestnpm i - Use to upgrade all dependencies
bun update --latest - Lock files (,
bun.lockb) ensure reproducible buildspackage-lock.json - 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.jsproject/
├── 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.jsArchitecture 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
undefinedmarkdown
undefinedProject 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 — 日志设置