chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "javascript-typescript",
|
||||
"version": "1.2.3",
|
||||
"description": "JavaScript and TypeScript development with ES6+, Node.js, React, and modern web frameworks",
|
||||
"author": {
|
||||
"name": "Seth Hobson",
|
||||
"email": "seth@major7apps.com"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "javascript-typescript",
|
||||
"version": "1.2.3",
|
||||
"description": "JavaScript and TypeScript development with ES6+, Node.js, React, and modern web frameworks",
|
||||
"skills": "./skills/",
|
||||
"author": {
|
||||
"name": "Seth Hobson",
|
||||
"email": "seth@major7apps.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"interface": {
|
||||
"displayName": "Javascript Typescript",
|
||||
"shortDescription": "JavaScript and TypeScript development with ES6+, Node.js, React, and modern web frameworks",
|
||||
"category": "Coding"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: javascript-pro
|
||||
description: Master modern JavaScript with ES6+, async patterns, and Node.js APIs. Handles promises, event loops, and browser/Node compatibility. Use PROACTIVELY for JavaScript optimization, async debugging, or complex JS patterns.
|
||||
model: inherit
|
||||
---
|
||||
|
||||
You are a JavaScript expert specializing in modern JS and async programming.
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- ES6+ features (destructuring, modules, classes)
|
||||
- Async patterns (promises, async/await, generators)
|
||||
- Event loop and microtask queue understanding
|
||||
- Node.js APIs and performance optimization
|
||||
- Browser APIs and cross-browser compatibility
|
||||
- TypeScript migration and type safety
|
||||
|
||||
## Approach
|
||||
|
||||
1. Prefer async/await over promise chains
|
||||
2. Use functional patterns where appropriate
|
||||
3. Handle errors at appropriate boundaries
|
||||
4. Avoid callback hell with modern patterns
|
||||
5. Consider bundle size for browser code
|
||||
|
||||
## Output
|
||||
|
||||
- Modern JavaScript with proper error handling
|
||||
- Async code with race condition prevention
|
||||
- Module structure with clean exports
|
||||
- Jest tests with async test patterns
|
||||
- Performance profiling results
|
||||
- Polyfill strategy for browser compatibility
|
||||
|
||||
Support both Node.js and browser environments. Include JSDoc comments.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: typescript-pro
|
||||
description: Master TypeScript with advanced types, generics, and strict type safety. Handles complex type systems, decorators, and enterprise-grade patterns. Use PROACTIVELY for TypeScript architecture, type inference optimization, or advanced typing patterns.
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are a TypeScript expert specializing in advanced typing and enterprise-grade development.
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Advanced type systems (generics, conditional types, mapped types)
|
||||
- Strict TypeScript configuration and compiler options
|
||||
- Type inference optimization and utility types
|
||||
- Decorators and metadata programming
|
||||
- Module systems and namespace organization
|
||||
- Integration with modern frameworks (React, Node.js, Express)
|
||||
|
||||
## Approach
|
||||
|
||||
1. Leverage strict type checking with appropriate compiler flags
|
||||
2. Use generics and utility types for maximum type safety
|
||||
3. Prefer type inference over explicit annotations when clear
|
||||
4. Design robust interfaces and abstract classes
|
||||
5. Implement proper error boundaries with typed exceptions
|
||||
6. Optimize build times with incremental compilation
|
||||
|
||||
## Output
|
||||
|
||||
- Strongly-typed TypeScript with comprehensive interfaces
|
||||
- Generic functions and classes with proper constraints
|
||||
- Custom utility types and advanced type manipulations
|
||||
- Jest/Vitest tests with proper type assertions
|
||||
- TSConfig optimization for project requirements
|
||||
- Type declaration files (.d.ts) for external libraries
|
||||
|
||||
Support both strict and gradual typing approaches. Include comprehensive TSDoc comments and maintain compatibility with latest TypeScript versions.
|
||||
@@ -0,0 +1,353 @@
|
||||
# TypeScript Project Scaffolding
|
||||
|
||||
You are a TypeScript project architecture expert specializing in scaffolding production-ready Node.js and frontend applications. Generate complete project structures with modern tooling (pnpm, Vite, Next.js), type safety, testing setup, and configuration following current best practices.
|
||||
|
||||
## Context
|
||||
|
||||
The user needs automated TypeScript project scaffolding that creates consistent, type-safe applications with proper structure, dependency management, testing, and build tooling. Focus on modern TypeScript patterns and scalable architecture.
|
||||
|
||||
## Requirements
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Analyze Project Type
|
||||
|
||||
Determine the project type from user requirements:
|
||||
|
||||
- **Next.js**: Full-stack React applications, SSR/SSG, API routes
|
||||
- **React + Vite**: SPA applications, component libraries
|
||||
- **Node.js API**: Express/Fastify backends, microservices
|
||||
- **Library**: Reusable packages, utilities, tools
|
||||
- **CLI**: Command-line tools, automation scripts
|
||||
|
||||
### 2. Initialize Project with pnpm
|
||||
|
||||
```bash
|
||||
# Install pnpm if needed
|
||||
npm install -g pnpm
|
||||
|
||||
# Initialize project
|
||||
mkdir project-name && cd project-name
|
||||
pnpm init
|
||||
|
||||
# Initialize git
|
||||
git init
|
||||
echo "node_modules/" >> .gitignore
|
||||
echo "dist/" >> .gitignore
|
||||
echo ".env" >> .gitignore
|
||||
```
|
||||
|
||||
### 3. Generate Next.js Project Structure
|
||||
|
||||
```bash
|
||||
# Create Next.js project with TypeScript
|
||||
pnpm create next-app@latest . --typescript --tailwind --app --src-dir --import-alias "@/*"
|
||||
```
|
||||
|
||||
```
|
||||
nextjs-project/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── next.config.js
|
||||
├── .env.example
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── layout.tsx
|
||||
│ │ ├── page.tsx
|
||||
│ │ ├── api/
|
||||
│ │ │ └── health/
|
||||
│ │ │ └── route.ts
|
||||
│ │ └── (routes)/
|
||||
│ │ └── dashboard/
|
||||
│ │ └── page.tsx
|
||||
│ ├── components/
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── Button.tsx
|
||||
│ │ │ └── Card.tsx
|
||||
│ │ └── layout/
|
||||
│ │ ├── Header.tsx
|
||||
│ │ └── Footer.tsx
|
||||
│ ├── lib/
|
||||
│ │ ├── api.ts
|
||||
│ │ ├── utils.ts
|
||||
│ │ └── types.ts
|
||||
│ └── hooks/
|
||||
│ ├── useAuth.ts
|
||||
│ └── useFetch.ts
|
||||
└── tests/
|
||||
├── setup.ts
|
||||
└── components/
|
||||
└── Button.test.tsx
|
||||
```
|
||||
|
||||
**package.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "nextjs-project",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^1.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**tsconfig.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "preserve",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [{ "name": "next" }]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate React + Vite Project Structure
|
||||
|
||||
```bash
|
||||
# Create Vite project
|
||||
pnpm create vite . --template react-ts
|
||||
```
|
||||
|
||||
**vite.config.ts**:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./tests/setup.ts",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Generate Node.js API Project Structure
|
||||
|
||||
```
|
||||
nodejs-api/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
│ ├── app.ts
|
||||
│ ├── config/
|
||||
│ │ ├── database.ts
|
||||
│ │ └── env.ts
|
||||
│ ├── routes/
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── users.ts
|
||||
│ │ └── health.ts
|
||||
│ ├── controllers/
|
||||
│ │ └── userController.ts
|
||||
│ ├── services/
|
||||
│ │ └── userService.ts
|
||||
│ ├── models/
|
||||
│ │ └── User.ts
|
||||
│ ├── middleware/
|
||||
│ │ ├── auth.ts
|
||||
│ │ └── errorHandler.ts
|
||||
│ └── types/
|
||||
│ └── express.d.ts
|
||||
└── tests/
|
||||
└── routes/
|
||||
└── users.test.ts
|
||||
```
|
||||
|
||||
**package.json for Node.js API**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "nodejs-api",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest",
|
||||
"lint": "eslint src --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.4.0",
|
||||
"zod": "^3.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vitest": "^1.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**src/app.ts**:
|
||||
|
||||
```typescript
|
||||
import express, { Express } from "express";
|
||||
import { healthRouter } from "./routes/health.js";
|
||||
import { userRouter } from "./routes/users.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
|
||||
export function createApp(): Express {
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use("/health", healthRouter);
|
||||
app.use("/api/users", userRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
return app;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Generate TypeScript Library Structure
|
||||
|
||||
```
|
||||
library-name/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── tsconfig.build.json
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
│ └── core.ts
|
||||
├── tests/
|
||||
│ └── core.test.ts
|
||||
└── dist/
|
||||
```
|
||||
|
||||
**package.json for Library**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@scope/library-name",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "vitest",
|
||||
"prepublishOnly": "pnpm build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^1.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Configure Development Tools
|
||||
|
||||
**.env.example**:
|
||||
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
||||
JWT_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
**vitest.config.ts**:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**.eslintrc.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
1. **Project Structure**: Complete directory tree with all necessary files
|
||||
2. **Configuration**: package.json, tsconfig.json, build tooling
|
||||
3. **Entry Point**: Main application file with type-safe setup
|
||||
4. **Tests**: Test structure with Vitest configuration
|
||||
5. **Documentation**: README with setup and usage instructions
|
||||
6. **Development Tools**: .env.example, .gitignore, linting config
|
||||
|
||||
Focus on creating production-ready TypeScript projects with modern tooling, strict type safety, and comprehensive testing setup.
|
||||
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: javascript-testing-patterns
|
||||
description: Implement comprehensive testing strategies using Jest, Vitest, and Testing Library for unit tests, integration tests, and end-to-end testing with mocking, fixtures, and test-driven development. Use when writing JavaScript/TypeScript tests, setting up test infrastructure, or implementing TDD/BDD workflows.
|
||||
---
|
||||
|
||||
# JavaScript Testing Patterns
|
||||
|
||||
Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up test infrastructure for new projects
|
||||
- Writing unit tests for functions and classes
|
||||
- Creating integration tests for APIs and services
|
||||
- Implementing end-to-end tests for user flows
|
||||
- Mocking external dependencies and APIs
|
||||
- Testing React, Vue, or other frontend components
|
||||
- Implementing test-driven development (TDD)
|
||||
- Setting up continuous testing in CI/CD pipelines
|
||||
|
||||
## Testing Frameworks
|
||||
|
||||
### Jest - Full-Featured Testing Framework
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/**/*.interface.ts",
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Vitest - Fast, Vite-Native Testing
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["**/*.d.ts", "**/*.config.ts", "**/dist/**"],
|
||||
},
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Unit Testing Patterns
|
||||
|
||||
### Pattern 1: Testing Pure Functions
|
||||
|
||||
```typescript
|
||||
// utils/calculator.ts
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
|
||||
// utils/calculator.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { add, divide } from "./calculator";
|
||||
|
||||
describe("Calculator", () => {
|
||||
describe("add", () => {
|
||||
it("should add two positive numbers", () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
|
||||
it("should add negative numbers", () => {
|
||||
expect(add(-2, -3)).toBe(-5);
|
||||
});
|
||||
|
||||
it("should handle zero", () => {
|
||||
expect(add(0, 5)).toBe(5);
|
||||
expect(add(5, 0)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("divide", () => {
|
||||
it("should divide two numbers", () => {
|
||||
expect(divide(10, 2)).toBe(5);
|
||||
});
|
||||
|
||||
it("should handle decimal results", () => {
|
||||
expect(divide(5, 2)).toBe(2.5);
|
||||
});
|
||||
|
||||
it("should throw error when dividing by zero", () => {
|
||||
expect(() => divide(10, 0)).toThrow("Division by zero");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Classes
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export class UserService {
|
||||
private users: Map<string, User> = new Map();
|
||||
|
||||
create(user: User): User {
|
||||
if (this.users.has(user.id)) {
|
||||
throw new Error("User already exists");
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
findById(id: string): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<User>): User {
|
||||
const user = this.users.get(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const updated = { ...user, ...updates };
|
||||
this.users.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
return this.users.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a new user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
const created = service.create(user);
|
||||
|
||||
expect(created).toEqual(user);
|
||||
expect(service.findById("1")).toEqual(user);
|
||||
});
|
||||
|
||||
it("should throw error if user already exists", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
expect(() => service.create(user)).toThrow("User already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("should update existing user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
const updated = service.update("1", { name: "Jane" });
|
||||
|
||||
expect(updated.name).toBe("Jane");
|
||||
expect(updated.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", () => {
|
||||
expect(() => service.update("999", { name: "Jane" })).toThrow(
|
||||
"User not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Testing Async Functions
|
||||
|
||||
```typescript
|
||||
// services/api.service.ts
|
||||
export class ApiService {
|
||||
async fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`https://api.example.com/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createUser(user: CreateUserDTO): Promise<User> {
|
||||
const response = await fetch("https://api.example.com/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// services/api.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ApiService } from "./api.service";
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("ApiService", () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ApiService();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchUser", () => {
|
||||
it("should fetch user successfully", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockUser,
|
||||
});
|
||||
|
||||
const user = await service.fetchUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
await expect(service.fetchUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const newUser = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...newUser };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => createdUser,
|
||||
});
|
||||
|
||||
const user = await service.createUser(newUser);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://api.example.com/users",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify(newUser),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Patterns
|
||||
|
||||
### Pattern 1: Mocking Modules
|
||||
|
||||
```typescript
|
||||
// services/email.service.ts
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export class EmailService {
|
||||
private transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string) {
|
||||
await this.transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// services/email.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EmailService } from "./email.service";
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn(() => ({
|
||||
sendMail: vi.fn().mockResolvedValue({ messageId: "123" }),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("EmailService", () => {
|
||||
let service: EmailService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new EmailService();
|
||||
});
|
||||
|
||||
it("should send email successfully", async () => {
|
||||
await service.sendEmail(
|
||||
"test@example.com",
|
||||
"Test Subject",
|
||||
"<p>Test Body</p>",
|
||||
);
|
||||
|
||||
expect(service["transporter"].sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@example.com",
|
||||
subject: "Test Subject",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection for Testing
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export interface IUserRepository {
|
||||
findById(id: string): Promise<User | null>;
|
||||
create(user: User): Promise<User>;
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: IUserRepository) {}
|
||||
|
||||
async getUser(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Business logic here
|
||||
const user = { id: generateId(), ...userData };
|
||||
return this.userRepository.create(user);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { UserService, IUserRepository } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
let mockRepository: IUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
findById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
};
|
||||
service = new UserService(mockRepository);
|
||||
});
|
||||
|
||||
describe("getUser", () => {
|
||||
it("should return user if found", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);
|
||||
|
||||
const user = await service.getUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(null);
|
||||
|
||||
await expect(service.getUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const userData = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...userData };
|
||||
|
||||
vi.mocked(mockRepository.create).mockResolvedValue(createdUser);
|
||||
|
||||
const user = await service.createUser(userData);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(mockRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Spying on Functions
|
||||
|
||||
```typescript
|
||||
// utils/logger.ts
|
||||
export const logger = {
|
||||
info: (message: string) => console.log(`INFO: ${message}`),
|
||||
error: (message: string) => console.error(`ERROR: ${message}`),
|
||||
};
|
||||
|
||||
// services/order.service.ts
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OrderService {
|
||||
async processOrder(orderId: string): Promise<void> {
|
||||
logger.info(`Processing order ${orderId}`);
|
||||
// Process order logic
|
||||
logger.info(`Order ${orderId} processed successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
// services/order.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { OrderService } from "./order.service";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
describe("OrderService", () => {
|
||||
let service: OrderService;
|
||||
let loggerSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new OrderService();
|
||||
loggerSpy = vi.spyOn(logger, "info");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should log order processing", async () => {
|
||||
await service.processOrder("123");
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Processing order 123");
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Order 123 processed successfully");
|
||||
expect(loggerSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
Integration tests verify real database operations and HTTP endpoints using `supertest` and a test database instance. Always truncate tables in `beforeEach` and tear down in `afterAll`.
|
||||
|
||||
For full API integration test examples (supertest + PostgreSQL) and database repository integration tests, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
Test React components by rendering them and querying by role, placeholder, or test ID. Test hooks with `renderHook` + `act`. Prefer semantic queries (`getByRole`, `getByPlaceholderText`) over `data-testid`.
|
||||
|
||||
For complete React component test examples (UserForm, hooks with `renderHook`/`act`), see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
Use `@faker-js/faker` to generate realistic test data factories. Factories accept optional `overrides` so tests can set only the fields they care about:
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function createUserFixture(overrides?: Partial<User>): User {
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
createdAt: faker.date.past(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For snapshot testing, coverage configuration, test organization patterns, promise testing, and timer mocking, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Follow AAA Pattern**: Arrange, Act, Assert
|
||||
2. **One assertion per test**: Or logically related assertions
|
||||
3. **Descriptive test names**: Should describe what is being tested
|
||||
4. **Use beforeEach/afterEach**: For setup and teardown
|
||||
5. **Mock external dependencies**: Keep tests isolated
|
||||
6. **Test edge cases**: Not just happy paths
|
||||
7. **Avoid implementation details**: Test behavior, not implementation
|
||||
8. **Use test factories**: For consistent test data
|
||||
9. **Keep tests fast**: Mock slow operations
|
||||
10. **Write tests first (TDD)**: When possible
|
||||
11. **Maintain test coverage**: Aim for 80%+ coverage
|
||||
12. **Use TypeScript**: For type-safe tests
|
||||
13. **Test error handling**: Not just success cases
|
||||
14. **Use data-testid sparingly**: Prefer semantic queries
|
||||
15. **Clean up after tests**: Prevent test pollution
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
# Advanced JavaScript Testing Patterns
|
||||
|
||||
Advanced patterns for integration testing, frontend component testing, fixtures, snapshot testing, coverage, and common test utilities.
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### Pattern 1: API Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.api.test.ts
|
||||
import request from "supertest";
|
||||
import { app } from "../../src/app";
|
||||
import { pool } from "../../src/config/database";
|
||||
|
||||
describe("User API Integration Tests", () => {
|
||||
beforeAll(async () => {
|
||||
// Setup test database
|
||||
await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear data before each test
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
describe("POST /api/users", () => {
|
||||
it("should create a new user", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
});
|
||||
expect(response.body).toHaveProperty("id");
|
||||
expect(response.body).not.toHaveProperty("password");
|
||||
});
|
||||
|
||||
it("should return 400 if email is invalid", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "invalid-email",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty("error");
|
||||
});
|
||||
|
||||
it("should return 409 if email already exists", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
await request(app).post("/api/users").send(userData);
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(409);
|
||||
|
||||
expect(response.body.error).toContain("already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/users/:id", () => {
|
||||
it("should get user by id", async () => {
|
||||
const createResponse = await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const userId = createResponse.body.id;
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/users/${userId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: userId,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 404 if user not found", async () => {
|
||||
await request(app).get("/api/users/999").expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should require authentication for protected routes", async () => {
|
||||
await request(app).get("/api/users/me").expect(401);
|
||||
});
|
||||
|
||||
it("should allow access with valid token", async () => {
|
||||
// Create user and login
|
||||
await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const loginResponse = await request(app).post("/api/auth/login").send({
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const token = loginResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/users/me")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.email).toBe("john@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Database Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.repository.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "../../src/repositories/user.repository";
|
||||
|
||||
describe("UserRepository Integration Tests", () => {
|
||||
let pool: Pool;
|
||||
let repository: UserRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
pool = new Pool({
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "test_db",
|
||||
user: "test_user",
|
||||
password: "test_password",
|
||||
});
|
||||
|
||||
repository = new UserRepository(pool);
|
||||
|
||||
// Create tables
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
it("should create a user", async () => {
|
||||
const user = await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
expect(user).toHaveProperty("id");
|
||||
expect(user.name).toBe("John Doe");
|
||||
expect(user.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should find user by email", async () => {
|
||||
await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
const user = await repository.findByEmail("john@example.com");
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user?.name).toBe("John Doe");
|
||||
});
|
||||
|
||||
it("should return null if user not found", async () => {
|
||||
const user = await repository.findByEmail("nonexistent@example.com");
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
### Pattern 1: React Component Testing
|
||||
|
||||
```typescript
|
||||
// components/UserForm.tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onSubmit: (user: { name: string; email: string }) => void;
|
||||
}
|
||||
|
||||
export function UserForm({ onSubmit }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ name, email });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// components/UserForm.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { UserForm } from './UserForm';
|
||||
|
||||
describe('UserForm', () => {
|
||||
it('should render form inputs', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should update input values', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
|
||||
const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } });
|
||||
fireEvent.change(emailInput, { target: { value: 'john@example.com' } });
|
||||
|
||||
expect(nameInput.value).toBe('John Doe');
|
||||
expect(emailInput.value).toBe('john@example.com');
|
||||
});
|
||||
|
||||
it('should call onSubmit with form data', () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<UserForm onSubmit={onSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('name-input'), {
|
||||
target: { value: 'John Doe' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('email-input'), {
|
||||
target: { value: 'john@example.com' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Hooks
|
||||
|
||||
```typescript
|
||||
// hooks/useCounter.ts
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const [count, setCount] = useState(initialValue);
|
||||
|
||||
const increment = useCallback(() => setCount((c) => c + 1), []);
|
||||
const decrement = useCallback(() => setCount((c) => c - 1), []);
|
||||
const reset = useCallback(() => setCount(initialValue), [initialValue]);
|
||||
|
||||
return { count, increment, decrement, reset };
|
||||
}
|
||||
|
||||
// hooks/useCounter.test.ts
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useCounter } from "./useCounter";
|
||||
|
||||
describe("useCounter", () => {
|
||||
it("should initialize with default value", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it("should initialize with custom value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
|
||||
it("should increment count", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it("should decrement count", () => {
|
||||
const { result } = renderHook(() => useCounter(5));
|
||||
|
||||
act(() => {
|
||||
result.current.decrement();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(4);
|
||||
});
|
||||
|
||||
it("should reset to initial value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(12);
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function createUserFixture(overrides?: Partial<User>): User {
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
createdAt: faker.date.past(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createUsersFixture(count: number): User[] {
|
||||
return Array.from({ length: count }, () => createUserFixture());
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
import {
|
||||
createUserFixture,
|
||||
createUsersFixture,
|
||||
} from "../fixtures/user.fixture";
|
||||
|
||||
describe("UserService", () => {
|
||||
it("should process user", () => {
|
||||
const user = createUserFixture({ name: "John Doe" });
|
||||
// Use user in test
|
||||
});
|
||||
|
||||
it("should handle multiple users", () => {
|
||||
const users = createUsersFixture(10);
|
||||
// Use users in test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Snapshot Testing
|
||||
|
||||
```typescript
|
||||
// components/UserCard.test.tsx
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserCard } from './UserCard';
|
||||
|
||||
describe('UserCard', () => {
|
||||
it('should match snapshot', () => {
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const { container } = render(<UserCard user={user} />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with loading state', () => {
|
||||
const { container } = render(<UserCard loading />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Coverage Reports
|
||||
|
||||
```typescript
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:ui": "vitest --ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
describe("UserService", () => {
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", () => {});
|
||||
it("should throw error if email exists", () => {});
|
||||
it("should hash password", () => {});
|
||||
});
|
||||
|
||||
describe("updateUser", () => {
|
||||
it("should update user", () => {});
|
||||
it("should throw error if not found", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Promises
|
||||
|
||||
```typescript
|
||||
// Using async/await
|
||||
it("should fetch user", async () => {
|
||||
const user = await service.fetchUser("1");
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
// Testing rejections
|
||||
it("should throw error", async () => {
|
||||
await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Timers
|
||||
|
||||
```typescript
|
||||
import { vi } from "vitest";
|
||||
|
||||
it("should call function after delay", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
setTimeout(callback, 1000);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: modern-javascript-patterns
|
||||
description: Master ES6+ features including async/await, destructuring, spread operators, arrow functions, promises, modules, iterators, generators, and functional programming patterns for writing clean, efficient JavaScript code. Use when refactoring legacy code, implementing modern patterns, or optimizing JavaScript applications.
|
||||
---
|
||||
|
||||
# Modern JavaScript Patterns
|
||||
|
||||
Comprehensive guide for mastering modern JavaScript (ES6+) features, functional programming patterns, and best practices for writing clean, maintainable, and performant code.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Refactoring legacy JavaScript to modern syntax
|
||||
- Implementing functional programming patterns
|
||||
- Optimizing JavaScript performance
|
||||
- Writing maintainable and readable code
|
||||
- Working with asynchronous operations
|
||||
- Building modern web applications
|
||||
- Migrating from callbacks to Promises/async-await
|
||||
- Implementing data transformation pipelines
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use const by default**: Only use let when reassignment is needed
|
||||
2. **Prefer arrow functions**: Especially for callbacks
|
||||
3. **Use template literals**: Instead of string concatenation
|
||||
4. **Destructure objects and arrays**: For cleaner code
|
||||
5. **Use async/await**: Instead of Promise chains
|
||||
6. **Avoid mutating data**: Use spread operator and array methods
|
||||
7. **Use optional chaining**: Prevent "Cannot read property of undefined"
|
||||
8. **Use nullish coalescing**: For default values
|
||||
9. **Prefer array methods**: Over traditional loops
|
||||
10. **Use modules**: For better code organization
|
||||
11. **Write pure functions**: Easier to test and reason about
|
||||
12. **Use meaningful variable names**: Self-documenting code
|
||||
13. **Keep functions small**: Single responsibility principle
|
||||
14. **Handle errors properly**: Use try/catch with async/await
|
||||
15. **Use strict mode**: `'use strict'` for better error catching
|
||||
|
||||
For common pitfalls (this binding, promise anti-patterns, memory leaks), see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
# Advanced Modern JavaScript Patterns
|
||||
|
||||
Advanced patterns covering functional programming, modern class features, ES6 modules, iterators, generators, modern operators, and performance optimization.
|
||||
|
||||
## Functional Programming Patterns
|
||||
|
||||
### 1. Array Methods
|
||||
|
||||
**Map, Filter, Reduce:**
|
||||
|
||||
```javascript
|
||||
const users = [
|
||||
{ id: 1, name: "John", age: 30, active: true },
|
||||
{ id: 2, name: "Jane", age: 25, active: false },
|
||||
{ id: 3, name: "Bob", age: 35, active: true },
|
||||
];
|
||||
|
||||
// Map - Transform array
|
||||
const names = users.map((user) => user.name);
|
||||
const upperNames = users.map((user) => user.name.toUpperCase());
|
||||
|
||||
// Filter - Select elements
|
||||
const activeUsers = users.filter((user) => user.active);
|
||||
const adults = users.filter((user) => user.age >= 18);
|
||||
|
||||
// Reduce - Aggregate data
|
||||
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
|
||||
const avgAge = totalAge / users.length;
|
||||
|
||||
// Group by property
|
||||
const byActive = users.reduce((groups, user) => {
|
||||
const key = user.active ? "active" : "inactive";
|
||||
return {
|
||||
...groups,
|
||||
[key]: [...(groups[key] || []), user],
|
||||
};
|
||||
}, {});
|
||||
|
||||
// Chaining methods
|
||||
const result = users
|
||||
.filter((user) => user.active)
|
||||
.map((user) => user.name)
|
||||
.sort()
|
||||
.join(", ");
|
||||
```
|
||||
|
||||
**Advanced Array Methods:**
|
||||
|
||||
```javascript
|
||||
// Find - First matching element
|
||||
const user = users.find((u) => u.id === 2);
|
||||
|
||||
// FindIndex - Index of first match
|
||||
const index = users.findIndex((u) => u.name === "Jane");
|
||||
|
||||
// Some - At least one matches
|
||||
const hasActive = users.some((u) => u.active);
|
||||
|
||||
// Every - All match
|
||||
const allAdults = users.every((u) => u.age >= 18);
|
||||
|
||||
// FlatMap - Map and flatten
|
||||
const userTags = [
|
||||
{ name: "John", tags: ["admin", "user"] },
|
||||
{ name: "Jane", tags: ["user"] },
|
||||
];
|
||||
const allTags = userTags.flatMap((u) => u.tags);
|
||||
|
||||
// From - Create array from iterable
|
||||
const str = "hello";
|
||||
const chars = Array.from(str);
|
||||
const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
|
||||
|
||||
// Of - Create array from arguments
|
||||
const arr = Array.of(1, 2, 3);
|
||||
```
|
||||
|
||||
### 2. Higher-Order Functions
|
||||
|
||||
**Functions as Arguments:**
|
||||
|
||||
```javascript
|
||||
// Custom forEach
|
||||
function forEach(array, callback) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
callback(array[i], i, array);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom map
|
||||
function map(array, transform) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
result.push(transform(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Custom filter
|
||||
function filter(array, predicate) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
if (predicate(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Functions Returning Functions:**
|
||||
|
||||
```javascript
|
||||
// Currying
|
||||
const multiply = (a) => (b) => a * b;
|
||||
const double = multiply(2);
|
||||
const triple = multiply(3);
|
||||
|
||||
console.log(double(5)); // 10
|
||||
console.log(triple(5)); // 15
|
||||
|
||||
// Partial application
|
||||
function partial(fn, ...args) {
|
||||
return (...moreArgs) => fn(...args, ...moreArgs);
|
||||
}
|
||||
|
||||
const add = (a, b, c) => a + b + c;
|
||||
const add5 = partial(add, 5);
|
||||
console.log(add5(3, 2)); // 10
|
||||
|
||||
// Memoization
|
||||
function memoize(fn) {
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = fn(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const fibonacci = memoize((n) => {
|
||||
if (n <= 1) return n;
|
||||
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Composition and Piping
|
||||
|
||||
```javascript
|
||||
// Function composition
|
||||
const compose =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), x);
|
||||
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
|
||||
// Example usage
|
||||
const addOne = (x) => x + 1;
|
||||
const double = (x) => x * 2;
|
||||
const square = (x) => x * x;
|
||||
|
||||
const composed = compose(square, double, addOne);
|
||||
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
const piped = pipe(addOne, double, square);
|
||||
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
// Practical example
|
||||
const processUser = pipe(
|
||||
(user) => ({ ...user, name: user.name.trim() }),
|
||||
(user) => ({ ...user, email: user.email.toLowerCase() }),
|
||||
(user) => ({ ...user, age: parseInt(user.age) }),
|
||||
);
|
||||
|
||||
const user = processUser({
|
||||
name: " John ",
|
||||
email: "JOHN@EXAMPLE.COM",
|
||||
age: "30",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Pure Functions and Immutability
|
||||
|
||||
```javascript
|
||||
// Impure function (modifies input)
|
||||
function addItemImpure(cart, item) {
|
||||
cart.items.push(item);
|
||||
cart.total += item.price;
|
||||
return cart;
|
||||
}
|
||||
|
||||
// Pure function (no side effects)
|
||||
function addItemPure(cart, item) {
|
||||
return {
|
||||
...cart,
|
||||
items: [...cart.items, item],
|
||||
total: cart.total + item.price,
|
||||
};
|
||||
}
|
||||
|
||||
// Immutable array operations
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
// Add to array
|
||||
const withSix = [...numbers, 6];
|
||||
|
||||
// Remove from array
|
||||
const withoutThree = numbers.filter((n) => n !== 3);
|
||||
|
||||
// Update array element
|
||||
const doubled = numbers.map((n) => (n === 3 ? n * 2 : n));
|
||||
|
||||
// Immutable object operations
|
||||
const user = { name: "John", age: 30 };
|
||||
|
||||
// Update property
|
||||
const olderUser = { ...user, age: 31 };
|
||||
|
||||
// Add property
|
||||
const withEmail = { ...user, email: "john@example.com" };
|
||||
|
||||
// Remove property
|
||||
const { age, ...withoutAge } = user;
|
||||
|
||||
// Deep cloning (simple approach)
|
||||
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
// Better deep cloning
|
||||
const structuredClone = (obj) => globalThis.structuredClone(obj);
|
||||
```
|
||||
|
||||
## Modern Class Features
|
||||
|
||||
```javascript
|
||||
// Class syntax
|
||||
class User {
|
||||
// Private fields
|
||||
#password;
|
||||
|
||||
// Public fields
|
||||
id;
|
||||
name;
|
||||
|
||||
// Static field
|
||||
static count = 0;
|
||||
|
||||
constructor(id, name, password) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.#password = password;
|
||||
User.count++;
|
||||
}
|
||||
|
||||
// Public method
|
||||
greet() {
|
||||
return `Hello, ${this.name}`;
|
||||
}
|
||||
|
||||
// Private method
|
||||
#hashPassword(password) {
|
||||
return `hashed_${password}`;
|
||||
}
|
||||
|
||||
// Getter
|
||||
get displayName() {
|
||||
return this.name.toUpperCase();
|
||||
}
|
||||
|
||||
// Setter
|
||||
set password(newPassword) {
|
||||
this.#password = this.#hashPassword(newPassword);
|
||||
}
|
||||
|
||||
// Static method
|
||||
static create(id, name, password) {
|
||||
return new User(id, name, password);
|
||||
}
|
||||
}
|
||||
|
||||
// Inheritance
|
||||
class Admin extends User {
|
||||
constructor(id, name, password, role) {
|
||||
super(id, name, password);
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
greet() {
|
||||
return `${super.greet()}, I'm an admin`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Modules (ES6)
|
||||
|
||||
```javascript
|
||||
// Exporting
|
||||
// math.js
|
||||
export const PI = 3.14159;
|
||||
export function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
export class Calculator {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Default export
|
||||
export default function multiply(a, b) {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
// Importing
|
||||
// app.js
|
||||
import multiply, { PI, add, Calculator } from "./math.js";
|
||||
|
||||
// Rename imports
|
||||
import { add as sum } from "./math.js";
|
||||
|
||||
// Import all
|
||||
import * as Math from "./math.js";
|
||||
|
||||
// Dynamic imports
|
||||
const module = await import("./math.js");
|
||||
const { add } = await import("./math.js");
|
||||
|
||||
// Conditional loading
|
||||
if (condition) {
|
||||
const module = await import("./feature.js");
|
||||
module.init();
|
||||
}
|
||||
```
|
||||
|
||||
## Iterators and Generators
|
||||
|
||||
```javascript
|
||||
// Custom iterator
|
||||
const range = {
|
||||
from: 1,
|
||||
to: 5,
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
current: this.from,
|
||||
last: this.to,
|
||||
|
||||
next() {
|
||||
if (this.current <= this.last) {
|
||||
return { done: false, value: this.current++ };
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
for (const num of range) {
|
||||
console.log(num); // 1, 2, 3, 4, 5
|
||||
}
|
||||
|
||||
// Generator function
|
||||
function* rangeGenerator(from, to) {
|
||||
for (let i = from; i <= to; i++) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
for (const num of rangeGenerator(1, 5)) {
|
||||
console.log(num);
|
||||
}
|
||||
|
||||
// Infinite generator
|
||||
function* fibonacci() {
|
||||
let [prev, curr] = [0, 1];
|
||||
while (true) {
|
||||
yield curr;
|
||||
[prev, curr] = [curr, prev + curr];
|
||||
}
|
||||
}
|
||||
|
||||
// Async generator
|
||||
async function* fetchPages(url) {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const response = await fetch(`${url}?page=${page}`);
|
||||
const data = await response.json();
|
||||
if (data.length === 0) break;
|
||||
yield data;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const page of fetchPages("/api/users")) {
|
||||
console.log(page);
|
||||
}
|
||||
```
|
||||
|
||||
## Modern Operators
|
||||
|
||||
```javascript
|
||||
// Optional chaining
|
||||
const user = { name: "John", address: { city: "NYC" } };
|
||||
const city = user?.address?.city;
|
||||
const zipCode = user?.address?.zipCode; // undefined
|
||||
|
||||
// Function call
|
||||
const result = obj.method?.();
|
||||
|
||||
// Array access
|
||||
const first = arr?.[0];
|
||||
|
||||
// Nullish coalescing
|
||||
const value = null ?? "default"; // 'default'
|
||||
const value = undefined ?? "default"; // 'default'
|
||||
const value = 0 ?? "default"; // 0 (not 'default')
|
||||
const value = "" ?? "default"; // '' (not 'default')
|
||||
|
||||
// Logical assignment
|
||||
let a = null;
|
||||
a ??= "default"; // a = 'default'
|
||||
|
||||
let b = 5;
|
||||
b ??= 10; // b = 5 (unchanged)
|
||||
|
||||
let obj = { count: 0 };
|
||||
obj.count ||= 1; // obj.count = 1
|
||||
obj.count &&= 2; // obj.count = 2
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
```javascript
|
||||
// Debounce
|
||||
function debounce(fn, delay) {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const searchDebounced = debounce(search, 300);
|
||||
|
||||
// Throttle
|
||||
function throttle(fn, limit) {
|
||||
let inThrottle;
|
||||
return (...args) => {
|
||||
if (!inThrottle) {
|
||||
fn(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const scrollThrottled = throttle(handleScroll, 100);
|
||||
|
||||
// Lazy evaluation
|
||||
function* lazyMap(iterable, transform) {
|
||||
for (const item of iterable) {
|
||||
yield transform(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Use only what you need
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
const doubled = lazyMap(numbers, (x) => x * 2);
|
||||
const first = doubled.next().value; // Only computes first value
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **this binding confusion**: Use arrow functions or bind()
|
||||
2. **Async/await without error handling**: Always use try/catch
|
||||
3. **Promise creation unnecessary**: Don't wrap already async functions
|
||||
4. **Mutation of objects**: Use spread operator or Object.assign()
|
||||
5. **Forgetting await**: Async functions return promises
|
||||
6. **Blocking event loop**: Avoid synchronous operations
|
||||
7. **Memory leaks**: Clean up event listeners and timers
|
||||
8. **Not handling promise rejections**: Use catch() or try/catch
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
# modern-javascript-patterns — detailed patterns and worked examples
|
||||
|
||||
## ES6+ Core Features
|
||||
|
||||
### 1. Arrow Functions
|
||||
|
||||
**Syntax and Use Cases:**
|
||||
|
||||
```javascript
|
||||
// Traditional function
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Arrow function
|
||||
const add = (a, b) => a + b;
|
||||
|
||||
// Single parameter (parentheses optional)
|
||||
const double = (x) => x * 2;
|
||||
|
||||
// No parameters
|
||||
const getRandom = () => Math.random();
|
||||
|
||||
// Multiple statements (need curly braces)
|
||||
const processUser = (user) => {
|
||||
const normalized = user.name.toLowerCase();
|
||||
return { ...user, name: normalized };
|
||||
};
|
||||
|
||||
// Returning objects (wrap in parentheses)
|
||||
const createUser = (name, age) => ({ name, age });
|
||||
```
|
||||
|
||||
**Lexical 'this' Binding:**
|
||||
|
||||
```javascript
|
||||
class Counter {
|
||||
constructor() {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
// Arrow function preserves 'this' context
|
||||
increment = () => {
|
||||
this.count++;
|
||||
};
|
||||
|
||||
// Traditional function loses 'this' in callbacks
|
||||
incrementTraditional() {
|
||||
setTimeout(function () {
|
||||
this.count++; // 'this' is undefined
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Arrow function maintains 'this'
|
||||
incrementArrow() {
|
||||
setTimeout(() => {
|
||||
this.count++; // 'this' refers to Counter instance
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Destructuring
|
||||
|
||||
**Object Destructuring:**
|
||||
|
||||
```javascript
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
address: {
|
||||
city: "New York",
|
||||
country: "USA",
|
||||
},
|
||||
};
|
||||
|
||||
// Basic destructuring
|
||||
const { name, email } = user;
|
||||
|
||||
// Rename variables
|
||||
const { name: userName, email: userEmail } = user;
|
||||
|
||||
// Default values
|
||||
const { age = 25 } = user;
|
||||
|
||||
// Nested destructuring
|
||||
const {
|
||||
address: { city, country },
|
||||
} = user;
|
||||
|
||||
// Rest operator
|
||||
const { id, ...userWithoutId } = user;
|
||||
|
||||
// Function parameters
|
||||
function greet({ name, age = 18 }) {
|
||||
console.log(`Hello ${name}, you are ${age}`);
|
||||
}
|
||||
greet(user);
|
||||
```
|
||||
|
||||
**Array Destructuring:**
|
||||
|
||||
```javascript
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
// Basic destructuring
|
||||
const [first, second] = numbers;
|
||||
|
||||
// Skip elements
|
||||
const [, , third] = numbers;
|
||||
|
||||
// Rest operator
|
||||
const [head, ...tail] = numbers;
|
||||
|
||||
// Swapping variables
|
||||
let a = 1,
|
||||
b = 2;
|
||||
[a, b] = [b, a];
|
||||
|
||||
// Function return values
|
||||
function getCoordinates() {
|
||||
return [10, 20];
|
||||
}
|
||||
const [x, y] = getCoordinates();
|
||||
|
||||
// Default values
|
||||
const [one, two, three = 0] = [1, 2];
|
||||
```
|
||||
|
||||
### 3. Spread and Rest Operators
|
||||
|
||||
**Spread Operator:**
|
||||
|
||||
```javascript
|
||||
// Array spreading
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [4, 5, 6];
|
||||
const combined = [...arr1, ...arr2];
|
||||
|
||||
// Object spreading
|
||||
const defaults = { theme: "dark", lang: "en" };
|
||||
const userPrefs = { theme: "light" };
|
||||
const settings = { ...defaults, ...userPrefs };
|
||||
|
||||
// Function arguments
|
||||
const numbers = [1, 2, 3];
|
||||
Math.max(...numbers);
|
||||
|
||||
// Copying arrays/objects (shallow copy)
|
||||
const copy = [...arr1];
|
||||
const objCopy = { ...user };
|
||||
|
||||
// Adding items immutably
|
||||
const newArr = [...arr1, 4, 5];
|
||||
const newObj = { ...user, age: 30 };
|
||||
```
|
||||
|
||||
**Rest Parameters:**
|
||||
|
||||
```javascript
|
||||
// Collect function arguments
|
||||
function sum(...numbers) {
|
||||
return numbers.reduce((total, num) => total + num, 0);
|
||||
}
|
||||
sum(1, 2, 3, 4, 5);
|
||||
|
||||
// With regular parameters
|
||||
function greet(greeting, ...names) {
|
||||
return `${greeting} ${names.join(", ")}`;
|
||||
}
|
||||
greet("Hello", "John", "Jane", "Bob");
|
||||
|
||||
// Object rest
|
||||
const { id, ...userData } = user;
|
||||
|
||||
// Array rest
|
||||
const [first, ...rest] = [1, 2, 3, 4, 5];
|
||||
```
|
||||
|
||||
### 4. Template Literals
|
||||
|
||||
```javascript
|
||||
// Basic usage
|
||||
const name = "John";
|
||||
const greeting = `Hello, ${name}!`;
|
||||
|
||||
// Multi-line strings
|
||||
const html = `
|
||||
<div>
|
||||
<h1>${title}</h1>
|
||||
<p>${content}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Expression evaluation
|
||||
const price = 19.99;
|
||||
const total = `Total: $${(price * 1.2).toFixed(2)}`;
|
||||
|
||||
// Tagged template literals
|
||||
function highlight(strings, ...values) {
|
||||
return strings.reduce((result, str, i) => {
|
||||
const value = values[i] || "";
|
||||
return result + str + `<mark>${value}</mark>`;
|
||||
}, "");
|
||||
}
|
||||
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
const html = highlight`Name: ${name}, Age: ${age}`;
|
||||
// Output: "Name: <mark>John</mark>, Age: <mark>30</mark>"
|
||||
```
|
||||
|
||||
### 5. Enhanced Object Literals
|
||||
|
||||
```javascript
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
|
||||
// Shorthand property names
|
||||
const user = { name, age };
|
||||
|
||||
// Shorthand method names
|
||||
const calculator = {
|
||||
add(a, b) {
|
||||
return a + b;
|
||||
},
|
||||
subtract(a, b) {
|
||||
return a - b;
|
||||
},
|
||||
};
|
||||
|
||||
// Computed property names
|
||||
const field = "email";
|
||||
const user = {
|
||||
name: "John",
|
||||
[field]: "john@example.com",
|
||||
[`get${field.charAt(0).toUpperCase()}${field.slice(1)}`]() {
|
||||
return this[field];
|
||||
},
|
||||
};
|
||||
|
||||
// Dynamic property creation
|
||||
const createUser = (name, ...props) => {
|
||||
return props.reduce(
|
||||
(user, [key, value]) => ({
|
||||
...user,
|
||||
[key]: value,
|
||||
}),
|
||||
{ name },
|
||||
);
|
||||
};
|
||||
|
||||
const user = createUser("John", ["age", 30], ["email", "john@example.com"]);
|
||||
```
|
||||
|
||||
## Asynchronous Patterns
|
||||
|
||||
### 1. Promises
|
||||
|
||||
**Creating and Using Promises:**
|
||||
|
||||
```javascript
|
||||
// Creating a promise
|
||||
const fetchUser = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (id > 0) {
|
||||
resolve({ id, name: "John" });
|
||||
} else {
|
||||
reject(new Error("Invalid ID"));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
// Using promises
|
||||
fetchUser(1)
|
||||
.then((user) => console.log(user))
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => console.log("Done"));
|
||||
|
||||
// Chaining promises
|
||||
fetchUser(1)
|
||||
.then((user) => fetchUserPosts(user.id))
|
||||
.then((posts) => processPosts(posts))
|
||||
.then((result) => console.log(result))
|
||||
.catch((error) => console.error(error));
|
||||
```
|
||||
|
||||
**Promise Combinators:**
|
||||
|
||||
```javascript
|
||||
// Promise.all - Wait for all promises
|
||||
const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];
|
||||
|
||||
Promise.all(promises)
|
||||
.then((users) => console.log(users))
|
||||
.catch((error) => console.error("At least one failed:", error));
|
||||
|
||||
// Promise.allSettled - Wait for all, regardless of outcome
|
||||
Promise.allSettled(promises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
console.log("Success:", result.value);
|
||||
} else {
|
||||
console.log("Error:", result.reason);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Promise.race - First to complete
|
||||
Promise.race(promises)
|
||||
.then((winner) => console.log("First:", winner))
|
||||
.catch((error) => console.error(error));
|
||||
|
||||
// Promise.any - First to succeed
|
||||
Promise.any(promises)
|
||||
.then((first) => console.log("First success:", first))
|
||||
.catch((error) => console.error("All failed:", error));
|
||||
```
|
||||
|
||||
### 2. Async/Await
|
||||
|
||||
**Basic Usage:**
|
||||
|
||||
```javascript
|
||||
// Async function always returns a Promise
|
||||
async function fetchUser(id) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const user = await response.json();
|
||||
return user;
|
||||
}
|
||||
|
||||
// Error handling with try/catch
|
||||
async function getUserData(id) {
|
||||
try {
|
||||
const user = await fetchUser(id);
|
||||
const posts = await fetchUserPosts(user.id);
|
||||
return { user, posts };
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential vs Parallel execution
|
||||
async function sequential() {
|
||||
const user1 = await fetchUser(1); // Wait
|
||||
const user2 = await fetchUser(2); // Then wait
|
||||
return [user1, user2];
|
||||
}
|
||||
|
||||
async function parallel() {
|
||||
const [user1, user2] = await Promise.all([fetchUser(1), fetchUser(2)]);
|
||||
return [user1, user2];
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced Patterns:**
|
||||
|
||||
```javascript
|
||||
// Async IIFE
|
||||
(async () => {
|
||||
const result = await someAsyncOperation();
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
// Async iteration
|
||||
async function processUsers(userIds) {
|
||||
for (const id of userIds) {
|
||||
const user = await fetchUser(id);
|
||||
await processUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level await (ES2022)
|
||||
const config = await fetch("/config.json").then((r) => r.json());
|
||||
|
||||
// Retry logic
|
||||
async function fetchWithRetry(url, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await fetch(url);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout wrapper
|
||||
async function withTimeout(promise, ms) {
|
||||
const timeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), ms),
|
||||
);
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
```
|
||||
|
||||
## Functional Programming Patterns
|
||||
|
||||
Functional programming in JavaScript centers on pure functions, immutability, and composable transformations.
|
||||
|
||||
Key topics covered in [references/advanced-patterns.md](references/advanced-patterns.md):
|
||||
- **Array methods** — `map`, `filter`, `reduce`, `find`, `findIndex`, `some`, `every`, `flatMap`, `Array.from`
|
||||
- **Higher-order functions** — custom `forEach`/`map`/`filter`, currying, partial application, memoization
|
||||
- **Composition and piping** — `compose`/`pipe` utilities with practical data transformation examples
|
||||
- **Pure functions and immutability** — immutable array/object operations, deep cloning with `structuredClone`
|
||||
|
||||
## Modern Class Features
|
||||
|
||||
ES2022 classes support private fields (`#field`), static fields, getters/setters, and private methods. See [references/advanced-patterns.md](references/advanced-patterns.md) for a full example with inheritance.
|
||||
|
||||
## Modules (ES6)
|
||||
|
||||
```javascript
|
||||
// Named exports
|
||||
export const PI = 3.14159;
|
||||
export function add(a, b) { return a + b; }
|
||||
|
||||
// Default export
|
||||
export default function multiply(a, b) { return a * b; }
|
||||
|
||||
// Import
|
||||
import multiply, { PI, add } from "./math.js";
|
||||
|
||||
// Dynamic import (code splitting)
|
||||
const { add } = await import("./math.js");
|
||||
```
|
||||
|
||||
For re-exports, namespace imports, and conditional dynamic loading see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Iterators and Generators
|
||||
|
||||
Generators (`function*`) and async generators (`async function*`) enable lazy sequences and async pagination. See [references/advanced-patterns.md](references/advanced-patterns.md) for custom iterator, range generator, fibonacci, and `for await...of` examples.
|
||||
|
||||
## Modern Operators
|
||||
|
||||
```javascript
|
||||
// Optional chaining — safe property access
|
||||
const city = user?.address?.city;
|
||||
const result = obj.method?.();
|
||||
|
||||
// Nullish coalescing — default only for null/undefined (not 0 or "")
|
||||
const value = null ?? "default"; // 'default'
|
||||
const zero = 0 ?? "default"; // 0
|
||||
|
||||
// Logical assignment
|
||||
a ??= "default"; // assign if null/undefined
|
||||
obj.count ||= 1; // assign if falsy
|
||||
obj.count &&= 2; // assign if truthy
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
See [references/advanced-patterns.md](references/advanced-patterns.md) for debounce, throttle, and lazy evaluation with generators.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: nodejs-backend-patterns
|
||||
description: Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
|
||||
---
|
||||
|
||||
# Node.js Backend Patterns
|
||||
|
||||
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building REST APIs or GraphQL servers
|
||||
- Creating microservices with Node.js
|
||||
- Implementing authentication and authorization
|
||||
- Designing scalable backend architectures
|
||||
- Setting up middleware and error handling
|
||||
- Integrating databases (SQL and NoSQL)
|
||||
- Building real-time applications with WebSockets
|
||||
- Implementing background job processing
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use TypeScript**: Type safety prevents runtime errors
|
||||
2. **Implement proper error handling**: Use custom error classes
|
||||
3. **Validate input**: Use libraries like Zod or Joi
|
||||
4. **Use environment variables**: Never hardcode secrets
|
||||
5. **Implement logging**: Use structured logging (Pino, Winston)
|
||||
6. **Add rate limiting**: Prevent abuse
|
||||
7. **Use HTTPS**: Always in production
|
||||
8. **Implement CORS properly**: Don't use `*` in production
|
||||
9. **Use dependency injection**: Easier testing and maintenance
|
||||
10. **Write tests**: Unit, integration, and E2E tests
|
||||
11. **Handle graceful shutdown**: Clean up resources
|
||||
12. **Use connection pooling**: For databases
|
||||
13. **Implement health checks**: For monitoring
|
||||
14. **Use compression**: Reduce response size
|
||||
15. **Monitor performance**: Use APM tools
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
See `javascript-testing-patterns` skill for comprehensive testing guidance.
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
# Node.js Advanced Patterns
|
||||
|
||||
Advanced patterns for dependency injection, database integration, authentication, caching, and API response formatting.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### DI Container
|
||||
|
||||
```typescript
|
||||
// di-container.ts
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "./repositories/user.repository";
|
||||
import { UserService } from "./services/user.service";
|
||||
import { UserController } from "./controllers/user.controller";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
|
||||
class Container {
|
||||
private instances = new Map<string, any>();
|
||||
|
||||
register<T>(key: string, factory: () => T): void {
|
||||
this.instances.set(key, factory);
|
||||
}
|
||||
|
||||
resolve<T>(key: string): T {
|
||||
const factory = this.instances.get(key);
|
||||
if (!factory) {
|
||||
throw new Error(`No factory registered for ${key}`);
|
||||
}
|
||||
return factory();
|
||||
}
|
||||
|
||||
singleton<T>(key: string, factory: () => T): void {
|
||||
let instance: T;
|
||||
this.instances.set(key, () => {
|
||||
if (!instance) {
|
||||
instance = factory();
|
||||
}
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const container = new Container();
|
||||
|
||||
// Register dependencies
|
||||
container.singleton(
|
||||
"db",
|
||||
() =>
|
||||
new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userRepository",
|
||||
() => new UserRepository(container.resolve("db")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userService",
|
||||
() => new UserService(container.resolve("userRepository")),
|
||||
);
|
||||
|
||||
container.register(
|
||||
"userController",
|
||||
() => new UserController(container.resolve("userService")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"authService",
|
||||
() => new AuthService(container.resolve("userRepository")),
|
||||
);
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### PostgreSQL with Connection Pool
|
||||
|
||||
```typescript
|
||||
// config/database.ts
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
};
|
||||
|
||||
export const pool = new Pool(poolConfig);
|
||||
|
||||
// Test connection
|
||||
pool.on("connect", () => {
|
||||
console.log("Database connected");
|
||||
});
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database error", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
export const closeDatabase = async () => {
|
||||
await pool.end();
|
||||
console.log("Database connection closed");
|
||||
};
|
||||
```
|
||||
|
||||
### MongoDB with Mongoose
|
||||
|
||||
```typescript
|
||||
// config/mongoose.ts
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGODB_URI!, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
console.log("MongoDB connected");
|
||||
} catch (error) {
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
mongoose.connection.on("disconnected", () => {
|
||||
console.log("MongoDB disconnected");
|
||||
});
|
||||
|
||||
mongoose.connection.on("error", (err) => {
|
||||
console.error("MongoDB error:", err);
|
||||
});
|
||||
|
||||
export { connectDB };
|
||||
|
||||
// Model example
|
||||
import { Schema, model, Document } from "mongoose";
|
||||
|
||||
interface IUser extends Document {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Indexes
|
||||
userSchema.index({ email: 1 });
|
||||
|
||||
export const User = model<IUser>("User", userSchema);
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
// services/order.service.ts
|
||||
import { Pool } from "pg";
|
||||
|
||||
export class OrderService {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async createOrder(userId: string, items: any[]) {
|
||||
const client = await this.db.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Create order
|
||||
const orderResult = await client.query(
|
||||
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
|
||||
[userId, calculateTotal(items)],
|
||||
);
|
||||
const orderId = orderResult.rows[0].id;
|
||||
|
||||
// Create order items
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
|
||||
[orderId, item.productId, item.quantity, item.price],
|
||||
);
|
||||
|
||||
// Update inventory
|
||||
await client.query(
|
||||
"UPDATE products SET stock = stock - $1 WHERE id = $2",
|
||||
[item.quantity, item.productId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return orderId;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
```typescript
|
||||
// services/auth.service.ts
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
export class AuthService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
const user = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
const refreshToken = this.generateRefreshToken({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string) {
|
||||
try {
|
||||
const payload = jwt.verify(
|
||||
refreshToken,
|
||||
process.env.REFRESH_TOKEN_SECRET!,
|
||||
) as { userId: string };
|
||||
|
||||
const user = await this.userRepository.findById(payload.userId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("User not found");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
return { token };
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError("Invalid refresh token");
|
||||
}
|
||||
}
|
||||
|
||||
private generateToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
||||
expiresIn: "15m",
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
```typescript
|
||||
// utils/cache.ts
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
});
|
||||
|
||||
export class CacheService {
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await redis.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
const serialized = JSON.stringify(value);
|
||||
if (ttl) {
|
||||
await redis.setex(key, ttl, serialized);
|
||||
} else {
|
||||
await redis.set(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await redis.del(key);
|
||||
}
|
||||
|
||||
async invalidatePattern(pattern: string): Promise<void> {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache decorator
|
||||
export function Cacheable(ttl: number = 300) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const cache = new CacheService();
|
||||
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
||||
|
||||
const cached = await cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await originalMethod.apply(this, args);
|
||||
await cache.set(cacheKey, result, ttl);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Format
|
||||
|
||||
```typescript
|
||||
// utils/response.ts
|
||||
import { Response } from "express";
|
||||
|
||||
export class ApiResponse {
|
||||
static success<T>(
|
||||
res: Response,
|
||||
data: T,
|
||||
message?: string,
|
||||
statusCode = 200,
|
||||
) {
|
||||
return res.status(statusCode).json({
|
||||
status: "success",
|
||||
message,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
||||
return res.status(statusCode).json({
|
||||
status: "error",
|
||||
message,
|
||||
...(errors && { errors }),
|
||||
});
|
||||
}
|
||||
|
||||
static paginated<T>(
|
||||
res: Response,
|
||||
data: T[],
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
) {
|
||||
return res.json({
|
||||
status: "success",
|
||||
data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,599 @@
|
||||
# nodejs-backend-patterns — detailed patterns and worked examples
|
||||
|
||||
## Core Frameworks
|
||||
|
||||
### Express.js - Minimalist Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import compression from "compression";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
|
||||
app.use(compression());
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
console.log(`${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Fastify - High Performance Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import Fastify from "fastify";
|
||||
import helmet from "@fastify/helmet";
|
||||
import cors from "@fastify/cors";
|
||||
import compress from "@fastify/compress";
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Plugins
|
||||
await fastify.register(helmet);
|
||||
await fastify.register(cors, { origin: true });
|
||||
await fastify.register(compress);
|
||||
|
||||
// Type-safe routes with schema validation
|
||||
fastify.post<{
|
||||
Body: { name: string; email: string };
|
||||
Reply: { id: string; name: string };
|
||||
}>(
|
||||
"/users",
|
||||
{
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name", "email"],
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1 },
|
||||
email: { type: "string", format: "email" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { name, email } = request.body;
|
||||
return { id: "123", name };
|
||||
},
|
||||
);
|
||||
|
||||
await fastify.listen({ port: 3000, host: "0.0.0.0" });
|
||||
```
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Layered Architecture
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Handle HTTP requests/responses
|
||||
├── services/ # Business logic
|
||||
├── repositories/ # Data access layer
|
||||
├── models/ # Data models
|
||||
├── middleware/ # Express/Fastify middleware
|
||||
├── routes/ # Route definitions
|
||||
├── utils/ # Helper functions
|
||||
├── config/ # Configuration
|
||||
└── types/ # TypeScript types
|
||||
```
|
||||
|
||||
**Controller Layer:**
|
||||
|
||||
```typescript
|
||||
// controllers/user.controller.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { UserService } from "../services/user.service";
|
||||
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
|
||||
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
async createUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const userData: CreateUserDTO = req.body;
|
||||
const user = await this.userService.createUser(userData);
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = await this.userService.getUserById(id);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates: UpdateUserDTO = req.body;
|
||||
const user = await this.userService.updateUser(id, updates);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await this.userService.deleteUser(id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Service Layer:**
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
|
||||
import { NotFoundError, ValidationError } from "../utils/errors";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Validation
|
||||
const existingUser = await this.userRepository.findByEmail(userData.email);
|
||||
if (existingUser) {
|
||||
throw new ValidationError("Email already exists");
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
|
||||
// Create user
|
||||
const user = await this.userRepository.create({
|
||||
...userData,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
||||
const user = await this.userRepository.update(id, updates);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
const deleted = await this.userRepository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Layer:**
|
||||
|
||||
```typescript
|
||||
// repositories/user.repository.ts
|
||||
import { Pool } from "pg";
|
||||
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
|
||||
|
||||
export class UserRepository {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async create(
|
||||
userData: CreateUserDTO & { password: string },
|
||||
): Promise<UserEntity> {
|
||||
const query = `
|
||||
INSERT INTO users (name, email, password)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, name, email, password, created_at, updated_at
|
||||
`;
|
||||
const { rows } = await this.db.query(query, [
|
||||
userData.name,
|
||||
userData.email,
|
||||
userData.password,
|
||||
]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE id = $1";
|
||||
const { rows } = await this.db.query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE email = $1";
|
||||
const { rows } = await this.db.query(query, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
|
||||
const fields = Object.keys(updates);
|
||||
const values = Object.values(updates);
|
||||
|
||||
const setClause = fields
|
||||
.map((field, idx) => `${field} = $${idx + 2}`)
|
||||
.join(", ");
|
||||
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const { rows } = await this.db.query(query, [id, ...values]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const query = "DELETE FROM users WHERE id = $1";
|
||||
const { rowCount } = await this.db.query(query, [id]);
|
||||
return rowCount > 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection
|
||||
|
||||
Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Middleware Patterns
|
||||
|
||||
### Authentication Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/auth.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: JWTPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedError("No token provided");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new UnauthorizedError("Invalid token"));
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return next(new UnauthorizedError("Not authenticated"));
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
|
||||
|
||||
if (!hasRole) {
|
||||
return next(new UnauthorizedError("Insufficient permissions"));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AnyZodObject, ZodError } from "zod";
|
||||
import { ValidationError } from "../utils/errors";
|
||||
|
||||
export const validate = (schema: AnyZodObject) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await schema.parseAsync({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
});
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const errors = error.errors.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
next(new ValidationError("Validation failed", errors));
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Usage with Zod
|
||||
import { z } from "zod";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
}),
|
||||
});
|
||||
|
||||
router.post("/users", validate(createUserSchema), userController.createUser);
|
||||
```
|
||||
|
||||
### Rate Limiting Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/rate-limit.middleware.ts
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
});
|
||||
|
||||
export const apiLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: "Too many requests from this IP, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
export const authLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:auth:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5, // Stricter limit for auth endpoints
|
||||
skipSuccessfulRequests: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Request Logging Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/logger.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import pino from "pino";
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
});
|
||||
|
||||
export const requestLogger = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const start = Date.now();
|
||||
|
||||
// Log response when finished
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
logger.info({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userAgent: req.headers["user-agent"],
|
||||
ip: req.ip,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export { logger };
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Custom Error Classes
|
||||
|
||||
```typescript
|
||||
// utils/errors.ts
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public statusCode: number = 500,
|
||||
public isOperational: boolean = true,
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
public errors?: any[],
|
||||
) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = "Resource not found") {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(message: string = "Unauthorized") {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message: string = "Forbidden") {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Error Handler
|
||||
|
||||
```typescript
|
||||
// middleware/error-handler.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "../utils/errors";
|
||||
import { logger } from "./logger.middleware";
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
status: "error",
|
||||
message: err.message,
|
||||
...(err instanceof ValidationError && { errors: err.errors }),
|
||||
});
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
logger.error({
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
// Don't leak error details in production
|
||||
const message =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "Internal server error"
|
||||
: err.message;
|
||||
|
||||
res.status(500).json({
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
export const asyncHandler = (
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
|
||||
) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
|
||||
|
||||
Key patterns covered in [references/advanced-patterns.md](references/advanced-patterns.md):
|
||||
- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown
|
||||
- **MongoDB with Mongoose** — connection management and schema definition
|
||||
- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
JWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full `AuthService` implementation with `bcrypt` password comparison in [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
Redis-backed `CacheService` with get/set/delete/invalidatePattern, plus a `@Cacheable` decorator for method-level caching. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## API Response Format
|
||||
|
||||
Standardized `ApiResponse` helper with `success`, `error`, and `paginated` static methods. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
@@ -0,0 +1,318 @@
|
||||
---
|
||||
name: typescript-advanced-types
|
||||
description: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.
|
||||
---
|
||||
|
||||
# TypeScript Advanced Types
|
||||
|
||||
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building type-safe libraries or frameworks
|
||||
- Creating reusable generic components
|
||||
- Implementing complex type inference logic
|
||||
- Designing type-safe API clients
|
||||
- Building form validation systems
|
||||
- Creating strongly-typed configuration objects
|
||||
- Implementing type-safe state management
|
||||
- Migrating JavaScript codebases to TypeScript
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Generics
|
||||
|
||||
**Purpose:** Create reusable, type-flexible components while maintaining type safety.
|
||||
|
||||
**Basic Generic Function:**
|
||||
|
||||
```typescript
|
||||
function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
const num = identity<number>(42); // Type: number
|
||||
const str = identity<string>("hello"); // Type: string
|
||||
const auto = identity(true); // Type inferred: boolean
|
||||
```
|
||||
|
||||
**Generic Constraints:**
|
||||
|
||||
```typescript
|
||||
interface HasLength {
|
||||
length: number;
|
||||
}
|
||||
|
||||
function logLength<T extends HasLength>(item: T): T {
|
||||
console.log(item.length);
|
||||
return item;
|
||||
}
|
||||
|
||||
logLength("hello"); // OK: string has length
|
||||
logLength([1, 2, 3]); // OK: array has length
|
||||
logLength({ length: 10 }); // OK: object has length
|
||||
// logLength(42); // Error: number has no length
|
||||
```
|
||||
|
||||
**Multiple Type Parameters:**
|
||||
|
||||
```typescript
|
||||
function merge<T, U>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 };
|
||||
}
|
||||
|
||||
const merged = merge({ name: "John" }, { age: 30 });
|
||||
// Type: { name: string } & { age: number }
|
||||
```
|
||||
|
||||
### 2. Conditional Types
|
||||
|
||||
**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.
|
||||
|
||||
**Basic Conditional Type:**
|
||||
|
||||
```typescript
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
```
|
||||
|
||||
**Extracting Return Types:**
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
|
||||
function getUser() {
|
||||
return { id: 1, name: "John" };
|
||||
}
|
||||
|
||||
type User = ReturnType<typeof getUser>;
|
||||
// Type: { id: number; name: string; }
|
||||
```
|
||||
|
||||
**Distributive Conditional Types:**
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
|
||||
type StrOrNumArray = ToArray<string | number>;
|
||||
// Type: string[] | number[]
|
||||
```
|
||||
|
||||
**Nested Conditions:**
|
||||
|
||||
```typescript
|
||||
type TypeName<T> = T extends string
|
||||
? "string"
|
||||
: T extends number
|
||||
? "number"
|
||||
: T extends boolean
|
||||
? "boolean"
|
||||
: T extends undefined
|
||||
? "undefined"
|
||||
: T extends Function
|
||||
? "function"
|
||||
: "object";
|
||||
|
||||
type T1 = TypeName<string>; // "string"
|
||||
type T2 = TypeName<() => void>; // "function"
|
||||
```
|
||||
|
||||
### 3. Mapped Types
|
||||
|
||||
**Purpose:** Transform existing types by iterating over their properties.
|
||||
|
||||
**Basic Mapped Type:**
|
||||
|
||||
```typescript
|
||||
type Readonly<T> = {
|
||||
readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ReadonlyUser = Readonly<User>;
|
||||
// Type: { readonly id: number; readonly name: string; }
|
||||
```
|
||||
|
||||
**Optional Properties:**
|
||||
|
||||
```typescript
|
||||
type Partial<T> = {
|
||||
[P in keyof T]?: T[P];
|
||||
};
|
||||
|
||||
type PartialUser = Partial<User>;
|
||||
// Type: { id?: number; name?: string; }
|
||||
```
|
||||
|
||||
**Key Remapping:**
|
||||
|
||||
```typescript
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>;
|
||||
// Type: { getName: () => string; getAge: () => number; }
|
||||
```
|
||||
|
||||
**Filtering Properties:**
|
||||
|
||||
```typescript
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
interface Mixed {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
type OnlyNumbers = PickByType<Mixed, number>;
|
||||
// Type: { id: number; age: number; }
|
||||
```
|
||||
|
||||
### 4. Template Literal Types
|
||||
|
||||
**Purpose:** Create string-based types with pattern matching and transformation.
|
||||
|
||||
**Basic Template Literal:**
|
||||
|
||||
```typescript
|
||||
type EventName = "click" | "focus" | "blur";
|
||||
type EventHandler = `on${Capitalize<EventName>}`;
|
||||
// Type: "onClick" | "onFocus" | "onBlur"
|
||||
```
|
||||
|
||||
**String Manipulation:**
|
||||
|
||||
```typescript
|
||||
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
|
||||
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
|
||||
type CapitalizedName = Capitalize<"john">; // "John"
|
||||
type UncapitalizedName = Uncapitalize<"John">; // "john"
|
||||
```
|
||||
|
||||
**Path Building:**
|
||||
|
||||
```typescript
|
||||
type Path<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
interface Config {
|
||||
server: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
database: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ConfigPath = Path<Config>;
|
||||
// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
|
||||
```
|
||||
|
||||
### 5. Utility Types
|
||||
|
||||
**Built-in Utility Types:**
|
||||
|
||||
```typescript
|
||||
// Partial<T> - Make all properties optional
|
||||
type PartialUser = Partial<User>;
|
||||
|
||||
// Required<T> - Make all properties required
|
||||
type RequiredUser = Required<PartialUser>;
|
||||
|
||||
// Readonly<T> - Make all properties readonly
|
||||
type ReadonlyUser = Readonly<User>;
|
||||
|
||||
// Pick<T, K> - Select specific properties
|
||||
type UserName = Pick<User, "name" | "email">;
|
||||
|
||||
// Omit<T, K> - Remove specific properties
|
||||
type UserWithoutPassword = Omit<User, "password">;
|
||||
|
||||
// Exclude<T, U> - Exclude types from union
|
||||
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
|
||||
|
||||
// Extract<T, U> - Extract types from union
|
||||
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
|
||||
|
||||
// NonNullable<T> - Exclude null and undefined
|
||||
type T3 = NonNullable<string | null | undefined>; // string
|
||||
|
||||
// Record<K, T> - Create object type with keys K and values T
|
||||
type PageInfo = Record<"home" | "about", { title: string }>;
|
||||
```
|
||||
|
||||
## Detailed worked examples and patterns
|
||||
|
||||
Detailed sections (starting with `## Advanced Patterns`) live in `references/details.md`. Read that file when the navigation summary above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `unknown` over `any`**: Enforce type checking
|
||||
2. **Prefer `interface` for object shapes**: Better error messages
|
||||
3. **Use `type` for unions and complex types**: More flexible
|
||||
4. **Leverage type inference**: Let TypeScript infer when possible
|
||||
5. **Create helper types**: Build reusable type utilities
|
||||
6. **Use const assertions**: Preserve literal types
|
||||
7. **Avoid type assertions**: Use type guards instead
|
||||
8. **Document complex types**: Add JSDoc comments
|
||||
9. **Use strict mode**: Enable all strict compiler options
|
||||
10. **Test your types**: Use type tests to verify type behavior
|
||||
|
||||
## Type Testing
|
||||
|
||||
```typescript
|
||||
// Type assertion tests
|
||||
type AssertEqual<T, U> = [T] extends [U]
|
||||
? [U] extends [T]
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
|
||||
type Test1 = AssertEqual<string, string>; // true
|
||||
type Test2 = AssertEqual<string, number>; // false
|
||||
type Test3 = AssertEqual<string | number, string>; // false
|
||||
|
||||
// Expect error helper
|
||||
type ExpectError<T extends never> = T;
|
||||
|
||||
// Example usage
|
||||
type ShouldError = ExpectError<AssertEqual<string, number>>;
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Over-using `any`**: Defeats the purpose of TypeScript
|
||||
2. **Ignoring strict null checks**: Can lead to runtime errors
|
||||
3. **Too complex types**: Can slow down compilation
|
||||
4. **Not using discriminated unions**: Misses type narrowing opportunities
|
||||
5. **Forgetting readonly modifiers**: Allows unintended mutations
|
||||
6. **Circular type references**: Can cause compiler errors
|
||||
7. **Not handling edge cases**: Like empty arrays or null values
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Avoid deeply nested conditional types
|
||||
- Use simple types when possible
|
||||
- Cache complex type computations
|
||||
- Limit recursion depth in recursive types
|
||||
- Use build tools to skip type checking in production
|
||||
@@ -0,0 +1,404 @@
|
||||
# typescript-advanced-types — detailed worked examples
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Pattern 1: Type-Safe Event Emitter
|
||||
|
||||
```typescript
|
||||
type EventMap = {
|
||||
"user:created": { id: string; name: string };
|
||||
"user:updated": { id: string };
|
||||
"user:deleted": { id: string };
|
||||
};
|
||||
|
||||
class TypedEventEmitter<T extends Record<string, any>> {
|
||||
private listeners: {
|
||||
[K in keyof T]?: Array<(data: T[K]) => void>;
|
||||
} = {};
|
||||
|
||||
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event]!.push(callback);
|
||||
}
|
||||
|
||||
emit<K extends keyof T>(event: K, data: T[K]): void {
|
||||
const callbacks = this.listeners[event];
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emitter = new TypedEventEmitter<EventMap>();
|
||||
|
||||
emitter.on("user:created", (data) => {
|
||||
console.log(data.id, data.name); // Type-safe!
|
||||
});
|
||||
|
||||
emitter.emit("user:created", { id: "1", name: "John" });
|
||||
// emitter.emit("user:created", { id: "1" }); // Error: missing 'name'
|
||||
```
|
||||
|
||||
### Pattern 2: Type-Safe API Client
|
||||
|
||||
```typescript
|
||||
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
type EndpointConfig = {
|
||||
"/users": {
|
||||
GET: { response: User[] };
|
||||
POST: { body: { name: string; email: string }; response: User };
|
||||
};
|
||||
"/users/:id": {
|
||||
GET: { params: { id: string }; response: User };
|
||||
PUT: { params: { id: string }; body: Partial<User>; response: User };
|
||||
DELETE: { params: { id: string }; response: void };
|
||||
};
|
||||
};
|
||||
|
||||
type ExtractParams<T> = T extends { params: infer P } ? P : never;
|
||||
type ExtractBody<T> = T extends { body: infer B } ? B : never;
|
||||
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
|
||||
|
||||
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
|
||||
async request<Path extends keyof Config, Method extends keyof Config[Path]>(
|
||||
path: Path,
|
||||
method: Method,
|
||||
...[options]: ExtractParams<Config[Path][Method]> extends never
|
||||
? ExtractBody<Config[Path][Method]> extends never
|
||||
? []
|
||||
: [{ body: ExtractBody<Config[Path][Method]> }]
|
||||
: [
|
||||
{
|
||||
params: ExtractParams<Config[Path][Method]>;
|
||||
body?: ExtractBody<Config[Path][Method]>;
|
||||
},
|
||||
]
|
||||
): Promise<ExtractResponse<Config[Path][Method]>> {
|
||||
// Implementation here
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient<EndpointConfig>();
|
||||
|
||||
// Type-safe API calls
|
||||
const users = await api.request("/users", "GET");
|
||||
// Type: User[]
|
||||
|
||||
const newUser = await api.request("/users", "POST", {
|
||||
body: { name: "John", email: "john@example.com" },
|
||||
});
|
||||
// Type: User
|
||||
|
||||
const user = await api.request("/users/:id", "GET", {
|
||||
params: { id: "123" },
|
||||
});
|
||||
// Type: User
|
||||
```
|
||||
|
||||
### Pattern 3: Builder Pattern with Type Safety
|
||||
|
||||
```typescript
|
||||
type BuilderState<T> = {
|
||||
[K in keyof T]: T[K] | undefined;
|
||||
};
|
||||
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type IsComplete<T, S> =
|
||||
RequiredKeys<T> extends keyof S
|
||||
? S[RequiredKeys<T>] extends undefined
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
class Builder<T, S extends BuilderState<T> = {}> {
|
||||
private state: S = {} as S;
|
||||
|
||||
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
|
||||
this.state[key] = value;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
build(this: IsComplete<T, S> extends true ? this : never): T {
|
||||
return this.state as T;
|
||||
}
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
age?: number;
|
||||
}
|
||||
|
||||
const builder = new Builder<User>();
|
||||
|
||||
const user = builder
|
||||
.set("id", "1")
|
||||
.set("name", "John")
|
||||
.set("email", "john@example.com")
|
||||
.build(); // OK: all required fields set
|
||||
|
||||
// const incomplete = builder
|
||||
// .set("id", "1")
|
||||
// .build(); // Error: missing required fields
|
||||
```
|
||||
|
||||
### Pattern 4: Deep Readonly/Partial
|
||||
|
||||
```typescript
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: T[P] extends object
|
||||
? T[P] extends Function
|
||||
? T[P]
|
||||
: DeepReadonly<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object
|
||||
? T[P] extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: DeepPartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
interface Config {
|
||||
server: {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: {
|
||||
enabled: boolean;
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
database: {
|
||||
url: string;
|
||||
pool: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type ReadonlyConfig = DeepReadonly<Config>;
|
||||
// All nested properties are readonly
|
||||
|
||||
type PartialConfig = DeepPartial<Config>;
|
||||
// All nested properties are optional
|
||||
```
|
||||
|
||||
### Pattern 5: Type-Safe Form Validation
|
||||
|
||||
```typescript
|
||||
type ValidationRule<T> = {
|
||||
validate: (value: T) => boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type FieldValidation<T> = {
|
||||
[K in keyof T]?: ValidationRule<T[K]>[];
|
||||
};
|
||||
|
||||
type ValidationErrors<T> = {
|
||||
[K in keyof T]?: string[];
|
||||
};
|
||||
|
||||
class FormValidator<T extends Record<string, any>> {
|
||||
constructor(private rules: FieldValidation<T>) {}
|
||||
|
||||
validate(data: T): ValidationErrors<T> | null {
|
||||
const errors: ValidationErrors<T> = {};
|
||||
let hasErrors = false;
|
||||
|
||||
for (const key in this.rules) {
|
||||
const fieldRules = this.rules[key];
|
||||
const value = data[key];
|
||||
|
||||
if (fieldRules) {
|
||||
const fieldErrors: string[] = [];
|
||||
|
||||
for (const rule of fieldRules) {
|
||||
if (!rule.validate(value)) {
|
||||
fieldErrors.push(rule.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldErrors.length > 0) {
|
||||
errors[key] = fieldErrors;
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasErrors ? errors : null;
|
||||
}
|
||||
}
|
||||
|
||||
interface LoginForm {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const validator = new FormValidator<LoginForm>({
|
||||
email: [
|
||||
{
|
||||
validate: (v) => v.includes("@"),
|
||||
message: "Email must contain @",
|
||||
},
|
||||
{
|
||||
validate: (v) => v.length > 0,
|
||||
message: "Email is required",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
validate: (v) => v.length >= 8,
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const errors = validator.validate({
|
||||
email: "invalid",
|
||||
password: "short",
|
||||
});
|
||||
// Type: { email?: string[]; password?: string[]; } | null
|
||||
```
|
||||
|
||||
### Pattern 6: Discriminated Unions
|
||||
|
||||
```typescript
|
||||
type Success<T> = {
|
||||
status: "success";
|
||||
data: T;
|
||||
};
|
||||
|
||||
type Error = {
|
||||
status: "error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Loading = {
|
||||
status: "loading";
|
||||
};
|
||||
|
||||
type AsyncState<T> = Success<T> | Error | Loading;
|
||||
|
||||
function handleState<T>(state: AsyncState<T>): void {
|
||||
switch (state.status) {
|
||||
case "success":
|
||||
console.log(state.data); // Type: T
|
||||
break;
|
||||
case "error":
|
||||
console.log(state.error); // Type: string
|
||||
break;
|
||||
case "loading":
|
||||
console.log("Loading...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe state machine
|
||||
type State =
|
||||
| { type: "idle" }
|
||||
| { type: "fetching"; requestId: string }
|
||||
| { type: "success"; data: any }
|
||||
| { type: "error"; error: Error };
|
||||
|
||||
type Event =
|
||||
| { type: "FETCH"; requestId: string }
|
||||
| { type: "SUCCESS"; data: any }
|
||||
| { type: "ERROR"; error: Error }
|
||||
| { type: "RESET" };
|
||||
|
||||
function reducer(state: State, event: Event): State {
|
||||
switch (state.type) {
|
||||
case "idle":
|
||||
return event.type === "FETCH"
|
||||
? { type: "fetching", requestId: event.requestId }
|
||||
: state;
|
||||
case "fetching":
|
||||
if (event.type === "SUCCESS") {
|
||||
return { type: "success", data: event.data };
|
||||
}
|
||||
if (event.type === "ERROR") {
|
||||
return { type: "error", error: event.error };
|
||||
}
|
||||
return state;
|
||||
case "success":
|
||||
case "error":
|
||||
return event.type === "RESET" ? { type: "idle" } : state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type Inference Techniques
|
||||
|
||||
### 1. Infer Keyword
|
||||
|
||||
```typescript
|
||||
// Extract array element type
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type NumArray = number[];
|
||||
type Num = ElementType<NumArray>; // number
|
||||
|
||||
// Extract promise type
|
||||
type PromiseType<T> = T extends Promise<infer U> ? U : never;
|
||||
|
||||
type AsyncNum = PromiseType<Promise<number>>; // number
|
||||
|
||||
// Extract function parameters
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
|
||||
function foo(a: string, b: number) {}
|
||||
type FooParams = Parameters<typeof foo>; // [string, number]
|
||||
```
|
||||
|
||||
### 2. Type Guards
|
||||
|
||||
```typescript
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function isArrayOf<T>(
|
||||
value: unknown,
|
||||
guard: (item: unknown) => item is T,
|
||||
): value is T[] {
|
||||
return Array.isArray(value) && value.every(guard);
|
||||
}
|
||||
|
||||
const data: unknown = ["a", "b", "c"];
|
||||
|
||||
if (isArrayOf(data, isString)) {
|
||||
data.forEach((s) => s.toUpperCase()); // Type: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Assertion Functions
|
||||
|
||||
```typescript
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Not a string");
|
||||
}
|
||||
}
|
||||
|
||||
function processValue(value: unknown) {
|
||||
assertIsString(value);
|
||||
// value is now typed as string
|
||||
console.log(value.toUpperCase());
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user