AgentSkillsCN

typescript-backend

TypeScript 后端开发标准。在编写 Node.js 服务、Express/NestJS 应用程序、Zod 数据校验,或 TypeScript API 时,可参考此标准。内容涵盖严格的类型检查、错误处理机制、测试模式,以及异步编程的最佳实践。

SKILL.md
--- frontmatter
name: typescript-backend
description: |
  TypeScript backend development standards. Use when writing Node.js services,
  Express/NestJS applications, Zod validation, or TypeScript APIs. Covers strict
  typing, error handling, testing patterns, and async best practices.
disposition: contextual
filePatterns:
  - "**/*.ts"
  - "**/*.tsx"
  - "**/tsconfig*.json"
  - "**/package.json"
version: 1.0.0

TypeScript Development Standards

Core Requirements

  • MUST target TypeScript 5.0+ and Node.js 22+ LTS for new projects
  • MUST enable strict mode: strict, strictNullChecks, noImplicitAny, noUncheckedIndexedAccess
  • MUST NOT use any type - use proper typing or unknown with type guards
  • MUST handle null and undefined explicitly with proper type guards

Naming Conventions

ElementConventionExample
Classes/InterfacesPascalCaseUserService, UserProfile
Methods/VariablescamelCasegetUserById, isActive
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNT
Fileskebab-caseuser-service.ts
  • Names MUST be descriptive and express intent clearly
  • SHOULD NOT use abbreviations or Hungarian notation

Type System

Use Interfaces For:

  • Object shapes that may be extended
  • Public API contracts
  • Model definitions
typescript
export interface User {
  id: string;
  email: string;
  profile: UserProfile;
}

Use Type Aliases For:

  • Union types, computed types, function signatures
typescript
export type UserStatus = 'active' | 'inactive' | 'pending';
export type UserHandler<T> = (params: T) => Promise<User>;
export type UserUpdateData = Partial<Pick<User, 'email' | 'profile'>>;

Generics

typescript
export interface Repository<T> {
  findById(id: string): Promise<T | null>;
  create(entity: Omit<T, 'id' | 'createdAt'>): Promise<T>;
  update(id: string, updates: Partial<T>): Promise<T>;
}

Error Handling

Custom Error Hierarchy

typescript
export class ServiceError extends Error {
  constructor(message: string, public readonly code?: string) {
    super(message);
    this.name = 'ServiceError';
  }
}

export class NotFoundError extends ServiceError {
  constructor(entity: string) {
    super(`${entity} not found`, 'NOT_FOUND');
  }
}

Error Handling Pattern

  • MUST handle exceptions meaningfully - no empty catch blocks
  • Re-throw known errors, wrap unknown errors with context
typescript
async createUser(data: CreateUserRequest): Promise<User> {
  try {
    return await this.repository.create(data);
  } catch (error) {
    if (error instanceof ServiceError) throw error;
    throw new ServiceError('Failed to create user');
  }
}

Validation

Use Zod for runtime validation with type inference:

typescript
import { z } from 'zod';

export const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
  organizationId: z.string().min(1),
});

export type CreateUserRequest = z.infer<typeof CreateUserSchema>;

// Usage
const validated = CreateUserSchema.parse(input);

Validation Middleware

typescript
export const validate = <T>(schema: z.ZodSchema<T>) =>
  (req: Request, res: Response, next: NextFunction) => {
    try {
      req.body = schema.parse(req.body);
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        res.status(400).json({ error: 'Validation failed', details: error.errors });
        return;
      }
      next(error);
    }
  };

Testing

Test Structure

typescript
describe('UserService', () => {
  let service: UserService;
  let mockRepo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockRepo = { findById: jest.fn(), create: jest.fn() };
    service = new UserService(mockRepo);
  });

  describe('createUser', () => {
    it('should create user with valid data', async () => {
      const input = { email: 'test@example.com', name: 'John' };
      mockRepo.create.mockResolvedValue({ id: '1', ...input });

      const result = await service.createUser(input);

      expect(result.id).toBe('1');
      expect(mockRepo.create).toHaveBeenCalledWith(input);
    });

    it('should throw for invalid email', async () => {
      await expect(service.createUser({ email: '' }))
        .rejects.toThrow(ServiceError);
    });
  });
});

Service Architecture

Dependency Injection

typescript
export class UserService {
  constructor(
    private readonly repository: UserRepository,
    private readonly logger: Logger,
  ) {}

  async getUserById(id: string): Promise<User | null> {
    const user = await this.repository.findById(id);
    if (!user) this.logger.warn(`User ${id} not found`);
    return user;
  }
}

API Development

Express Middleware

typescript
export interface AuthenticatedRequest extends Request {
  user: User;
}

export const authMiddleware = async (
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction,
) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    res.status(401).json({ error: 'Authentication required' });
    return;
  }
  req.user = await validateToken(token);
  next();
};

Request Context

typescript
export interface RequestContext {
  user: User;
  traceId: string;
}

export const createContext = async (req: Request): Promise<RequestContext> => ({
  user: await getUserFromRequest(req),
  traceId: req.headers['x-trace-id'] as string || crypto.randomUUID(),
});

Async Patterns

Parallel Execution

typescript
// Independent operations - run in parallel
const [users, activities] = await Promise.all([
  this.userService.getUsers(orgId),
  this.activityService.getActivities(dateRange),
]);

// Dependent operations - run sequentially
const user = await this.userService.getUser(id);
const permissions = await this.permissionService.getForUser(user);

Caching

typescript
export class CachedService<T> {
  constructor(private cache: Cache, private ttl = 300) {}

  async getOrFetch(key: string, fetcher: () => Promise<T>): Promise<T> {
    const cached = await this.cache.get(key);
    if (cached) return JSON.parse(cached);

    const data = await fetcher();
    await this.cache.set(key, JSON.stringify(data), this.ttl);
    return data;
  }
}

Security

XSS Prevention and Output Encoding

  • MUST NOT use regex-based HTML sanitization (bypassed via event handlers, data: URLs, malformed tags)
  • MUST use context-appropriate output encoding at render time
  • MUST use well-vetted sanitization libraries when needed

Output Encoding by Context

typescript
// HTML Context - use templating engine auto-escaping (e.g., React, Vue, Angular)
// Or use a library like 'escape-html'
import escapeHtml from 'escape-html';
const safeHtml = escapeHtml(userInput);

// HTML Attribute Context - use proper attribute encoding
import { escape } from 'html-escaper';
const safeAttr = escape(userInput);

// JavaScript Context - use a dedicated library for proper escaping
// Note: Avoid inline <script> tags when possible; prefer external scripts
import serialize from 'serialize-javascript';
const safeJs = serialize(userInput);

// DO NOT USE for untrusted user input (shown for reference only)
// const safeJs = JSON.stringify(userInput).replace(/<\//g, '<\\/');
// WARNING: The above only escapes closing script tags, NOT other JS context XSS vectors
// This is INSUFFICIENT for security purposes - always use serialize-javascript instead

// URL Context - use encodeURIComponent
const safeUrl = encodeURIComponent(userInput);

Rich HTML Sanitization

When accepting rich HTML input (e.g., WYSIWYG editors), use a vetted library:

typescript
import DOMPurify from 'isomorphic-dompurify';
import { JSDOM } from 'jsdom';

// Sanitize rich HTML with safe defaults and add security attributes to external links
export const sanitizeRichHtml = (html: string): string => {
  const config = {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
    // Basic URL scheme validation - adjust pattern based on requirements
    ALLOWED_URI_REGEXP: /^(?:https?:\/\/.*|mailto:.*)$/i,
    ALLOW_DATA_ATTR: false,
    RETURN_DOM: false,
    RETURN_DOM_FRAGMENT: false,
  };

  // First, sanitize the HTML
  const clean = DOMPurify.sanitize(html, config);

  // Then, post-process to add security attributes to external links
  // This approach is concurrency-safe as each call creates its own isolated DOM
  const dom = new JSDOM(clean);
  const links = dom.window.document.querySelectorAll('a[href]');

  links.forEach((link) => {
    const href = link.getAttribute('href');
    if (href && /^https?:\/\/.*$/i.test(href)) {
      link.setAttribute('target', '_blank');
      link.setAttribute('rel', 'noopener noreferrer');
    }
  });

  return dom.window.document.body.innerHTML;

  /* DEPRECATED: Hook-based approach removed due to concurrency issues
   *
   * The code below was removed because DOMPurify hooks modify global shared state,
   * which causes race conditions in server-side Node.js environments where multiple
   * requests are processed concurrently. The current implementation above uses
   * a separate DOM parsing step after sanitization, which avoids global state
   * mutation and is safe for concurrent request handling in all environments.
   *
   * OLD CODE (DO NOT USE):
   *
   * // Use a one-time hook for this sanitization
   * DOMPurify.addHook('afterSanitizeAttributes', function(node) {
   *   if (node.tagName === 'A' && node.hasAttribute('href')) {
   *     const href = node.getAttribute('href');
   *     if (href && /^https?:\/\/.*$/i.test(href)) {
   *       node.setAttribute('target', '_blank');
   *       node.setAttribute('rel', 'noopener noreferrer');
   *     }
   *   }
   * });
   *
   * const clean = DOMPurify.sanitize(html, config);
   *
   * // Remove hook after use to avoid side effects
   * DOMPurify.removeAllHooks();
   *
   * return clean;
   */
};

// Alternative: Lightweight approach using linkedom (faster, smaller footprint)
export const sanitizeRichHtmlLightweight = async (html: string): Promise<string> => {
  const { parseHTML } = await import('linkedom');

  const config = {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
    ALLOWED_URI_REGEXP: /^(?:https?:\/\/.*|mailto:.*)$/i,
    ALLOW_DATA_ATTR: false,
    RETURN_DOM: false,
    RETURN_DOM_FRAGMENT: false,
  };

  const clean = DOMPurify.sanitize(html, config);
  const { document } = parseHTML(clean);
  const links = document.querySelectorAll('a[href]');

  links.forEach((link: Element) => {
    const href = link.getAttribute('href');
    if (href && /^https?:\/\/.*$/i.test(href)) {
      link.setAttribute('target', '_blank');
      link.setAttribute('rel', 'noopener noreferrer');
    }
  });

  return document.body.innerHTML;
};

Why this approach is better:

  1. Concurrency-safe: Each function call creates its own isolated DOM instance (no global state)
  2. Separation of concerns: Sanitization (security) is separate from transformation (UX enhancement)
  3. Testable: Easy to unit test each step independently
  4. Performance options: Choose jsdom for full compatibility or linkedom for better performance
  5. No race conditions: Unlike DOMPurify hooks, this doesn't mutate shared global state

Dependencies needed:

bash
npm install jsdom @types/jsdom
# OR for lightweight alternative
npm install linkedom

Environment Configuration

typescript
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  PORT: z.string().transform(Number),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
});

export const env = envSchema.parse(process.env);

Code Structure

Methods

  • MUST focus on single responsibility
  • SHOULD NOT exceed 5 parameters - use objects
  • SHOULD break complex logic into smaller testable units

Quality Rules

  • MUST remove duplicated code blocks
  • MUST replace magic numbers with named constants
  • MUST close resources properly (try-finally or using)
  • SHOULD simplify nested logic - avoid deep if trees

Anti-Patterns to Avoid

Anti-PatternCorrect Approach
Using anyUse proper types or unknown with guards
Ignoring null/undefinedHandle explicitly with type guards
Empty catch blocksHandle or re-throw with context
Unreachable codeRemove dead code after return/throw
Ignoring compiler warningsFix all warnings
Magic numbersUse named constants
Raw object accessValidate before accessing

Configuration References

  • TypeScript: Enable strict, noImplicitAny, strictNullChecks, noUncheckedIndexedAccess, noUnusedLocals
  • ESLint: Use @typescript-eslint/recommended with no-explicit-any: error
  • Prettier: Consistent formatting with team settings

These standards ensure type safety, consistency, maintainability, and testability across all TypeScript backend services.