Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Modern TypeScript project architecture guide for 2025. Use when creating new TS projects, setting up configurations, or designing project structure. Covers tech stack selection, layered architecture, and best practices.
.claude/skills/majiayu000-typescript-project/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 113% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 117% | 0% |
any, Zod for runtime validation> Delete unused code. Change directly. No compatibility layers.
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// ✅ 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// ❌ 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
grep -r "oldName" src/> Use LiteLLM proxy for all LLM integrations. Don't call provider APIs directly.
bash# Run LiteLLM proxy (Docker) docker run -p 4000:4000 ghcr.io/berriai/litellm:main-stable # Or install locally pip install litellm[proxy] litellm --model gpt-4o
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// ❌ 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, });
bash# Using Bun (recommended) bun init bun add zod bun add -d typescript @types/bun @biomejs/biome # Using Node.js npm init -y npm i zod npm i -D typescript @types/node tsx @biomejs/biome
| Layer | Recommendation | |-------|----------------| | Runtime | Bun / Node 22+ | | Language | TypeScript (latest) | | Validation | Zod (latest) | | Testing | Bun test / Vitest | | Build | bun build / tsup | | Linting | Biome (latest) |
> Always use latest. Never pin versions in templates.
json{ "dependencies": { "zod": "latest" }, "devDependencies": { "@biomejs/biome": "latest", "typescript": "latest" } }
bun add / npm i automatically fetches latestbun update --latest to upgrade all dependenciesbun.lockb, package-lock.json) ensure reproducible buildsproject/
├── 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.jsFoundational 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-lite skill)
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)); } }
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; } }
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{ "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" } }
typescriptimport { 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// 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// 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'); }); });
markdown## Project Setup - [ ] TypeScript strict mode enabled - [ ] ESM modules configured - [ ] Biome/ESLint configured - [ ] Testing framework ready ## Architecture - [ ] lib/ for core utilities - [ ] services/ for business logic - [ ] adapters/ for external integrations - [ ] Clear module boundaries ## Quality - [ ] Zod schemas for validation - [ ] Custom error classes - [ ] Structured logging - [ ] Tests for critical paths ## Build - [ ] Build script configured - [ ] Type checking in CI - [ ] Tests in CI
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 12,813 | 5,926 | -54% | 1 | 1 | 0% | 2,356 | 4,441 | +88% | 0 | 0 | — |
case-02 | fail→pass | 14,859 | 11,870 | -20% | 1 | 1 | 0% | 2,835 | 6,026 | +113% | 0 | 0 | — |
case-03 | fail→pass | 17,671 | 10,504 | -41% | 1 | 1 | 0% | 3,631 | 5,717 | +57% | 0 | 0 | — |
case-04 | fail→pass | 9,861 | 5,218 | -47% | 1 | 1 | 0% | 1,719 | 4,350 | +153% | 0 | 0 | — |
case-05 | fail→pass | 11,524 | 5,369 | -53% | 1 | 1 | 0% | 1,933 | 4,194 | +117% | 0 | 0 | — |
case-06 | fail→pass | 10,532 | 3,895 | -63% | 1 | 1 | 0% | 1,694 | 4,034 | +138% | 0 | 0 | — |
case-07 | fail→pass | 12,315 | 4,333 | -65% | 1 | 1 | 0% | 1,899 | 4,099 | +116% | 0 | 0 | — |
case-12 | fail→pass | 7,005 | 7,289 | +4% | 1 | 1 | 0% | 1,303 | 4,761 | +265% | 0 | 0 | — |
case-08 | pass→pass | 12,527 | 5,658 | -55% | 1 | 1 | 0% | 2,182 | 4,366 | +100% | 0 | 0 | — |
case-09 | fail→pass | 20,298 | 11,406 | -44% | 1 | 1 | 0% | 3,313 | 5,305 | +60% | 0 | 0 | — |
case-10 | pass→pass | 8,581 | 4,990 | -42% | 1 | 1 | 0% | 1,453 | 4,190 | +188% | 0 | 0 | — |
case-11 | pass→pass | 16,569 | 13,827 | -17% | 1 | 1 | 0% | 3,345 | 6,374 | +91% | 0 | 0 | — |
case-13 | pass→pass | 9,796 | 6,096 | -38% | 1 | 1 | 0% | 1,849 | 4,416 | +139% | 0 | 0 | — |
case-14 | fail→pass | 13,175 | 9,969 | -24% | 1 | 1 | 0% | 2,469 | 5,166 | +109% | 0 | 0 | — |
case-15 | pass→pass | 18,208 | 10,781 | -41% | 1 | 1 | 0% | 3,359 | 5,600 | +67% | 0 | 0 | — |
case-16 | fail→pass | 9,602 | 4,661 | -51% | 1 | 1 | 0% | 1,741 | 4,285 | +146% | 0 | 0 | — |
case-17 | fail→fail | 14,778 | 9,829 | -33% | 1 | 1 | 0% | 2,510 | 5,094 | +103% | 0 | 0 | — |
case-18 | pass→pass | 12,030 | 4,388 | -64% | 1 | 1 | 0% | 1,910 | 4,130 | +116% | 0 | 0 | — |
case-19 | pass→pass | 21,799 | 13,116 | -40% | 1 | 1 | 0% | 3,941 | 5,953 | +51% | 0 | 0 | — |
case-20 | pass→pass | 17,626 | 16,146 | -8% | 1 | 1 | 0% | 3,216 | 6,550 | +104% | 0 | 0 | — |
case-21 | pass→pass | 17,503 | 17,498 | -0% | 1 | 1 | 0% | 3,251 | 6,822 | +110% | 0 | 0 | — |
case-22 | pass→pass | 8,990 | 7,195 | -20% | 1 | 1 | 0% | 1,775 | 4,844 | +173% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.