# Code Style and Conventions Standards for TypeScript
This document outlines coding style and conventions standards for TypeScript development. Adhering to these standards promotes code consistency, readability, maintainability, and collaboration within development teams. These guidelines are tailored for the latest version of TypeScript and aim to leverage modern best practices.
## 1. Formatting
Consistent formatting is crucial for readability. We adopt the following standards for TypeScript code formatting:
### 1.1. Whitespace and Indentation
* **Standard:** Use 2 spaces for indentation. Avoid tabs.
* **Why:** Consistent indentation enhances readability and reduces visual clutter.
* **Do This:**
"""typescript
function calculateArea(width: number, height: number): number {
const area = width * height;
return area;
}
"""
* **Don't Do This:**
"""typescript
function calculateArea(width: number, height: number): number {
const area = width * height;
return area;
}
"""
* **Standard:** Use blank lines to separate logical sections of code within functions and classes.
* **Why:** Separating logical blocks improves code comprehension.
* **Do This:**
"""typescript
function processData(data: any[]): void {
// Validate data
if (!data || data.length === 0) {
throw new Error("Data is invalid.");
}
// Transform data
const transformedData = data.map(item => ({
...item,
processed: true,
}));
// Save data
saveToDatabase(transformedData);
}
"""
### 1.2. Line Length
* **Standard:** Limit lines to a maximum of 120 characters.
* **Why:** Enforces readability on various screen sizes and IDE configurations.
* **How:** Configure your editor or IDE to display a line length guide at 120 characters. Break long lines at logical points, such as after commas, operators, or before opening parentheses.
* **Do This:**
"""typescript
const veryLongVariableName = calculateSomethingComplicated(
param1,
param2,
param3
);
"""
* **Don't Do This:**
"""typescript
const veryLongVariableName = calculateSomethingComplicated(param1, param2, param3);
"""
### 1.3. Braces and Parentheses
* **Standard:** Use braces for all control flow statements, even single-line statements.
* **Why:** Improves code clarity and reduces potential errors when modifying code.
* **Do This:**
"""typescript
if (isValid) {
console.log("Valid");
} else {
console.log("Invalid");
}
"""
* **Don't Do This:**
"""typescript
if (isValid) console.log("Valid");
else console.log("Invalid");
"""
* **Standard:** Use parentheses to clarify operator precedence, where needed.
* **Why:** Reduces ambiguity, especially in complex expressions.
* **Example:**
"""typescript
const result = (a + b) * c;
"""
### 1.4. Semicolons
* **Standard:** Always use semicolons to terminate statements.
* **Why:** Prevents unexpected behavior due to JavaScript's automatic semicolon insertion (ASI).
* **Do This:**
"""typescript
const name = "John";
console.log(name);
"""
* **Don't Do This:**
"""typescript
const name = "John"
console.log(name)
"""
## 2. Naming Conventions
Consistent naming conventions are essential for code clarity and maintainability.
### 2.1. Variables and Constants
* **Standard:** Use camelCase for variable and constant names.
* **Why:** Widely adopted convention for JavaScript and TypeScript.
* **Do This:**
"""typescript
const userName = "Alice";
let itemCount = 0;
"""
* **Standard:** Use UPPER_SNAKE_CASE for constant values (i.e. values that may be inlined for performance or are known at compile time).
* **Why:** Clearly distinguishes constants from variables.
* **Do This:**
"""typescript
const MAX_RETRIES = 3;
const API_ENDPOINT = "https://example.com/api";
"""
### 2.2. Functions and Methods
* **Standard:** Use camelCase for function and method names.
* **Why:** Follows common JavaScript/TypeScript conventions.
* **Do This:**
"""typescript
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
class ShoppingCart {
addItem(item: string): void {
console.log("Adding ${item} to the cart.");
}
}
"""
### 2.3. Classes and Interfaces
* **Standard:** Use PascalCase for class and interface names.
* **Why:** Clearly identifies classes and interfaces.
* **Do This:**
"""typescript
interface User {
id: number;
name: string;
}
class Product {
constructor(public name: string, public price: number) {}
}
"""
### 2.4. Type Parameters
* **Standard:** Use single uppercase letters, typically "T", "U", "V", etc., for generic type parameters.
* **Why:** Follows established TypeScript conventions.
* **Do This:**
"""typescript
function identity(arg: T): T {
return arg;
}
"""
### 2.5. Boolean Variables
* **Standard:** Prefix boolean variables with "is", "has", or "should" to indicate a boolean value.
* **Why:** Improves readability by clearly indicating the purpose of the variable.
* **Do This:**
"""typescript
let isValid: boolean = true;
let hasPermission: boolean = false;
let shouldUpdate: boolean = true;
"""
## 3. Stylistic Consistency
Consistency in style is critical for code maintainability.
### 3.1. Type Annotations and Inference
* **Standard:** Use explicit type annotations where type inference is not obvious, especially for function parameters and return types.
* **Why:** Improves code clarity and helps catch type-related errors early.
* **Do This:**
"""typescript
function greet(name: string): string {
return "Hello, ${name}!";
}
const add: (x: number, y: number) => number = (x, y) => x + y;
"""
* **Don't Do This:**
"""typescript
function greet(name) { // Implicit 'any' type
return "Hello, ${name}!";
}
"""
* **Standard:** Leverage type inference for local variables when the type is immediately apparent.
* **Why:** Reduces verbosity and keeps code concise.
* **Do This:**
"""typescript
const message = "Hello, world!"; // Type inferred as string
const count = 10; // Type inferred as number
"""
* **Standard:** When initializing variables with "null" or "undefined", explicitly define the type.
* **Why:** Helps avoid unexpected type-related issues later.
* **Do This:**
"""typescript
let user: User | null = null;
let data: string[] | undefined = undefined;
"""
### 3.2. String Usage
* **Standard:** Prefer template literals for string concatenation and multi-line strings.
* **Why:** More readable and easier to maintain compared to traditional string concatenation.
* **Do This:**
"""typescript
const name = "Alice";
const message = "Hello, ${name}!";
const multiLine = "This is a
multi-line string.";
"""
* **Don't Do This:**
"""typescript
const name = "Alice";
const message = "Hello, " + name + "!";
const multiLine = "This is a\n" +
"multi-line string.";
"""
### 3.3. Object Literals
* **Standard:** Use shorthand notation for object properties when the property name matches the variable name.
* **Why:** Improves code conciseness and readability.
* **Do This:**
"""typescript
const name = "Alice";
const age = 30;
const user = { name, age }; // Shorthand notation
"""
* **Don't Do This:**
"""typescript
const name = "Alice";
const age = 30;
const user = { name: name, age: age };
"""
* **Standard:** Use object spread syntax for creating copies of objects or merging objects.
* **Why:** More concise and readable than older methods like "Object.assign()".
* **Do This:**
"""typescript
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // Creates a new object with a, b, and c
"""
### 3.4. Arrow Functions
* **Standard:** Use arrow functions for concise function expressions, especially for callbacks and inline functions.
* **Why:** More compact syntax and lexically binds "this".
* **Do This:**
"""typescript
const numbers = [1, 2, 3];
const squared = numbers.map(x => x * x); // Concise arrow function
"""
* **Don't Do This:**
"""typescript
const numbers = [1, 2, 3];
const squared = numbers.map(function(x) {
return x * x;
});
"""
* **Standard:** Omit parentheses for single-parameter arrow functions.
* **Why:** Makes the code even more concise.
* **Do This:**
"""typescript
const increment = x => x + 1;
"""
* **Don't Do This:**
"""typescript
const increment = (x) => x + 1;
"""
### 3.5. Modern TypeScript Features
* **Standard:** Leverage features like optional chaining ("?.") and nullish coalescing ("??") for safer and more concise code. Optional properties on interfaces may be relevant if these are in use.
* **Why:** Reduces boilerplate and improves null/undefined handling.
* **Do This:**
"""typescript
interface User {
profile?: {
address?: {
city?: string;
}
}
}
const user: User = {};
const city = user?.profile?.address?.city ?? "Unknown"; // Nullish coalescing
console.log(city);
interface Config {
timeout?: number;
}
const defaultConfig: Config = {
timeout: 5000,
};
function initialize(config?: Config) {
const timeout = config?.timeout ?? defaultConfig.timeout;
console.log("Timeout: ${timeout}");
}
initialize(); // Timeout: 5000
initialize({ timeout: 10000 }); // Timeout: 10000
"""
* **Standard:** Utilize discriminated unions and exhaustive checks for increased type safety and maintainability.
* **Why:** Improves type correctness and makes it easier to handle different states or object types.
* **Do This:**
"""typescript
interface Success {
type: "success";
result: any;
}
interface Error {
type: "error";
message: string;
}
type Result = Success | Error;
function handleResult(result: Result) {
switch (result.type) {
case "success":
console.log("Success:", result.result);
break;
case "error":
console.error("Error:", result.message);
break;
default:
// Exhaustive check: TypeScript will flag this if a new type is added to Result
const _exhaustiveCheck: never = result;
return _exhaustiveCheck;
}
}
const successResult: Success = { type: "success", result: { data: "example" } };
const errorResult: Error = { type: "error", message: "Something went wrong" };
handleResult(successResult);
handleResult(errorResult);
"""
### 3.6. Asynchronous Code
* **Standard:** Always use "async/await" syntax for asynchronous operations.
* **Why:** Improves readability and simplifies error handling compared to traditional promise chains.
* **Do This:**
"""typescript
async function fetchData(): Promise {
try {
const response = await fetch("https://example.com/api/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching data:", error);
throw error;
}
}
"""
* **Don't Do This:**
"""typescript
function fetchData(): Promise {
return fetch("https://example.com/api/data")
.then(response => response.json())
.then(data => data)
.catch(error => {
console.error("Error fetching data:", error);
throw error;
});
}
"""
* **Standard:** Use "Promise.all" for concurrent asynchronous operations that don't depend on each other.
* **Why:** Improves performance by executing asynchronous tasks in parallel.
* **Do This:**
"""typescript
async function processData(): Promise {
const [result1, result2] = await Promise.all([
fetchData1(),
fetchData2(),
]);
console.log("Result 1:", result1);
console.log("Result 2:", result2);
}
"""
### 3.7 Error Handling
* **Standard:** Implement robust error handling using "try...catch" blocks, especially in asynchronous functions.
* **Why:** Prevents unhandled exceptions and allows for graceful recovery.
* **Do This:**
"""typescript
async function doSomething() {
try {
const result = await someAsyncOperation();
console.log("Result:", result);
} catch (error) {
console.error("An error occurred:", error);
// Implement specific error handling/logging
}
}
"""
* **Standard:** Create custom error classes to provide more context and specify error handling logic.
* **Why:** Extends the built-in Error to communicate more specific information about the error to the outside world.
* **Do This:**
"""typescript
class CustomError extends Error {
constructor(message: string, public errorCode: number) {
super(message);
this.name = "CustomError";
Object.setPrototypeOf(this, CustomError.prototype);
}
}
async function performOperation() {
try {
// some operation
throw new CustomError("Operation failed", 500);
} catch (error) {
if (error instanceof CustomError) {
console.error("Custom Error ${error.errorCode}: ${error.message}");
} else {
console.error("An unexpected error occurred:", error);
}
}
}
"""
### 3.8 Immutability
* **Standard:** Strive for immutability whenever practical, using "const" for variables that should not be reassigned and avoiding direct modification of objects and arrays.
* **Why:** Makes code more predictable and easier to reason about, reducing the risk of bugs.
* **Do This:**
"""typescript
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4]; // Creates a new array
const originalObject = { a: 1, b: 2 };
const newObject = { ...originalObject, c: 3 }; // Creates a new object
"""
* **Don't Do This:**
"""typescript
const originalArray = [1, 2, 3];
originalArray.push(4); // Modifies the original array
const originalObject = { a: 1, b: 2 };
originalObject.c = 3; // Modifies the original object
"""
### 3.9 Comments
* **Standard:** Add comments to explain complex or non-obvious logic, but prioritize writing self-documenting code.
* **Why:** Comments should supplement, not replace, clear code.
* **Guidelines:**
* Use JSDoc-style comments for documenting functions, classes, and interfaces.
* Explain the *why*, not the *what*. The code should explain what it does.
* Keep comments concise and up-to-date.
* Remove outdated or redundant comments.
* **Example:**
"""typescript
/**
* Calculates the area of a rectangle.
* @param width The width of the rectangle.
* @param height The height of the rectangle.
* @returns The area of the rectangle.
*/
function calculateArea(width: number, height: number): number {
return width * height;
}
"""
## 4. Technology Specific Details (Distinguishing Good from Great)
* **Standard:** Take advantage of TypeScript's advanced type features to create robust and maintainable code structures.
* **Guidelines:**
* **Utility Types:** Employ TypeScript's utility types ("Partial", "Readonly", "Pick", "Omit", "Record", etc.) to manipulate types and generate new types efficiently.
"""typescript
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Make all properties optional
type PartialUser = Partial;
// Make specified properties required
type RequiredIdAndName = Required>;
// Type with only certain properties
type UserInfo = Pick;
// Type without certain properties
type UserWithoutId = Omit;
"""
* **Mapped Types:** Utilize mapped types to transform the properties of an existing type, providing a dynamic way to define new types based on existing ones.
"""typescript
interface Product {
id: string;
name: string;
price: number;
}
// Create a read-only version of Product
type ReadonlyProduct = Readonly;
// Create a type where all properties of Product are nullable
type NullableProduct = {
[K in keyof Product]: Product[K] | null;
};
"""
* **Conditional Types:** Conditional types allow to define types based on conditions, adding yet another powerful layer of abstraction to type definitions. They help to ensure type safety throughout an application.
"""typescript
type NonNullableProperty = T[K] extends null | undefined ? never : K;
type RequiredProperties = Pick>;
interface Configuration {
host?: string;
port?: number; // Port can be potentially undefined or null
timeout?: number;
}
// Extracts properties that are guaranteed to be assigned during runtime.
type RuntimeProperties = RequiredProperties;
// Result equivalent to {timeout: number;}
"""
* **Decorators:** Use decorators to add metadata or modify the behavior of classes, methods, properties, or parameters.
* **Why:** Provide a declarative and reusable way to add functionality, such as logging, validation, or dependency injection.
* **Example:**
"""typescript
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log("Calling method ${propertyKey} with arguments: ${JSON.stringify(args)}");
const result = originalMethod.apply(this, args);
console.log("Method ${propertyKey} returned: ${result}");
return result;
};
return descriptor;
}
class Calculator {
@logMethod
add(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3);
// Output:
// Calling method add with arguments: [2,3]
// Method add returned: 5
"""
## 5. Tooling
* **Standard:** Use Prettier for automatic code formatting and ESLint with recommended TypeScript rules for linting.
* **Why:** Automates formatting and enforces code quality, reducing manual effort and improving consistency.
* **Configuration:** Configure Prettier and ESLint to work seamlessly with your IDE and CI/CD pipeline.
* **Example ".eslintrc.js" configuration:**
"""javascript
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
project: './tsconfig.json',
},
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier",
],
rules: {
// Add or override rules here
},
};
"""
By adhering to these coding standards, TypeScript projects will benefit from improved code quality, readability, and maintainability, fostering a collaborative and productive development environment.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# TypeScript Performance Optimization Standards: Best Practices for Efficient Applications This document outlines coding standards and best practices specifically for performance optimization in TypeScript projects. Adhering to these guidelines will improve the speed, responsiveness, efficient use of resources, and overall user experience of your applications. ## Table of Contents - [1. Architectural Considerations for Performance](#1-architectural-considerations-for-performance) - [1.1. Code Splitting](#11-code-splitting) - [1.2. Lazy Loading Modules](#12-lazy-loading-modules) - [1.3. Server-Side Rendering (SSR) or Static Site Generation (SSG)](#13-server-side-rendering-ssr-or-static-site-generation-ssg) - [1.4. Data Structure Selection](#14-data-structure-selection) ## 1. Architectural Considerations for Performance ### 1.1. Code Splitting **Standard:** Implement code splitting to reduce the initial load time of your application. **Why:** Loading only the necessary code on initial page load significantly improves the user experience. **Do This:** * Utilize dynamic imports (`import()`) to load modules on demand. * Configure your bundler (Webpack, Parcel, Rollup) to create separate chunks for different parts of your application. **Don't Do This:** * Load the entire application code in a single bundle. * Use `require()` statements (CommonJS) in modern TypeScript projects where ES Modules are supported. **Example:** ```typescript // Before: Loading everything upfront import { featureA } from './featureA'; import { featureB } from './featureB'; // After: Code splitting with dynamic imports async function loadFeatureA() { const { featureA } = await import('./featureA'); featureA.init(); } async function loadFeatureB() { const { featureB } = await import('./featureB'); featureB.init(); } // Use loadFeatureA or loadFeatureB based on user interaction or route ``` **Bundler Configuration (Webpack example):** ```javascript // webpack.config.js module.exports = { entry: './src/index.ts', output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, optimization: { splitChunks: { chunks: 'all', // Split all chunks of code }, }, }; ``` ### 1.2. Lazy Loading Modules **Standard:** Employ lazy loading for non-critical modules or components. **Why:** Reduce the amount of code that needs to be parsed and compiled on initial load. **Do This:** * Load components or modules only when they are needed. * Utilize Intersection Observer API to load components when they become visible in the viewport. **Don't Do This:** * Load modules that are not immediately required for the current user interaction. **Example (Intersection Observer Lazy Loading):** ```typescript function lazyLoadComponent(element: HTMLElement, importPath: string) { const observer = new IntersectionObserver((entries) => { entries.forEach(async (entry) => { if (entry.isIntersecting) { const { default: Component } = await import(importPath); const componentInstance = new Component(); // Instantiate the component. element.appendChild(componentInstance.render()); // Append to the DOM (adjust according to your framework). observer.unobserve(element); } }); }); observer.observe(element); } // Usage: const lazyComponentElement = document.getElementById('lazy-component'); if (lazyComponentElement) { lazyLoadComponent(lazyComponentElement, './MyHeavyComponent'); } ``` ### 1.3. Server-Side Rendering (SSR) or Static Site Generation (SSG) **Standard:** Consider using SSR or SSG for content-heavy, SEO-sensitive, or performance-critical applications. **Why:** Reduces the time to first paint (TTFP) and improves SEO by providing crawlers with pre-rendered content. **Do This:** * Evaluate the trade-offs between SSR, SSG, and client-side rendering (CSR) based on your application's needs. * Use frameworks like Next.js (React), Nuxt.js (Vue), or Angular Universal. * Implement appropriate caching strategies for SSR. **Don't Do This:** * Default to CSR when SSR or SSG could provide significant performance benefits. **Example (Next.js):** ```typescript // pages/index.tsx (Next.js example) import React from 'react'; interface Props { data: { title: string; description: string; }; } const HomePage: React.FC<Props> = ({ data }) => { return ( <div> <h1>{data.title}</h1> <p>{data.description}</p> </div> ); }; export async function getServerSideProps() { // Fetch data from an API, database, or file system. const data = { title: 'My Awesome Website', description: 'Welcome to my extremely performant website!', }; return { props: { data, }, }; } export default HomePage; ``` ### 1.4. Data Structure Selection **Standard:** Select the most appropriate data structure for each specific use case. **Why:** Using appropriate data structures will reduce the complexity and improve the execution speed of algorithms. **Do This:** * Use `Map` when you need to associate keys with values, especially when the keys are not strings or numbers. * Use `Set` when you need to store a collection of unique values. * Use `Record<K, V>` type for type-safe object mapping. * Consider specialized data structures for specific performance needs (e.g., priority queues, linked lists). **Don't Do This:** * Use generic arrays or objects when more specialized data structures would be more efficient. * Perform frequent lookups in arrays when using a Map or Set would be more performant. **Example:** ```typescript // Before: Using an array for lookups const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' } ]; // O(n) lookup operation const findUser = (id: number) => users.find(user => user.id === id); // After: Using Map for efficient lookups const userMap = new Map<number, {id: number, name: string}>(); userMap.set(1, { id: 1, name: 'Alice' }); userMap.set(2, { id: 2, name: 'Bob' }); userMap.set(3, { id: 3, name: 'Charlie' }); // O(1) lookup operation const getUser = (id: number) => userMap.get(id); ```
# Security Best Practices Standards for TypeScript This document outlines security best practices for TypeScript development. These standards are designed to help developers write secure, maintainable, and performant code. By adhering to these guidelines, we can minimize vulnerabilities and create more robust applications. ## 1. Input Validation and Sanitization ### Standard Always validate and sanitize user input on both the client and server sides. **Why:** Preventing common injection attacks (XSS, SQLi, command injection). **Do This:** * Use validation libraries to enforce expected formats and constraints. * Sanitize input to remove or escape potentially harmful characters. * Implement allow-lists rather than deny-lists for predictable security. **Don't Do This:** * Trust user input without validation. * Rely solely on client-side validation, as it can be bypassed. * Use regular expressions as the *only* validation step, especially regexes constructed dynamically. **Code Examples:** TypeScript function to validate and sanitize user input: """typescript /** * Validates email format and sanitizes it to prevent XSS attacks. * @param email The email address to validate and santize * @returns The sanitized email if valid, null otherwise */ function validateAndSanitizeEmail(email: string): string | null { if (typeof email !== 'string') { return null; // Ensure it's a string } // Basic email format validation using regex, can be replaced with more advanced libraries. const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return null; // Invalid email format } // Sanitize email to prevent XSS: replace <, >, and " const sanitizedEmail = email.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); return sanitizedEmail; } // Usage example const userInputEmail = '<script>alert("XSS")</script>test@example.com'; const sanitized = validateAndSanitizeEmail(userInputEmail); if (sanitized) { console.log("Valid and Sanitized email: ", sanitized); // Outputs: Valid and Sanitized email: <script>alert("XSS")</script>test@example.com } else { console.error("Invalid Email"); } """ **Anti-Pattern:** """typescript // Vulnerable code - no input validation or sanitization function displayUserInput(input: string): void { document.getElementById('output').innerHTML = input; // Prone to XSS } displayUserInput(userInput); """ ## 2. Authentication and Authorization ### Standard Implement robust authentication and authorization mechanisms, avoiding common pitfalls. **Why:** Protecting sensitive data and resources from unauthorized access. **Do This:** * Use strong password hashing algorithms (e.g., bcrypt or Argon2). * Implement multi-factor authentication (MFA) where possible/necessary. * Apply the principle of least privilege (POLP) for user roles and permissions. * Use established libraries that support best-practice security measures. **Don't Do This:** * Store passwords in plaintext or using weak hashing algorithms (e.g., MD5, SHA1). * Grant excessive permissions to users or roles. * Rely solely on cookies for authentication. **Code Examples:** Password hashing with bcrypt: """typescript import bcrypt from 'bcrypt'; async function hashPassword(password: string): Promise<string> { const saltRounds = 10; // Number of salt rounds impacts security and performance. const hashedPassword = await bcrypt.hash(password, saltRounds); return hashedPassword; } async function comparePassword(password: string, hash: string): Promise<boolean> { return await bcrypt.compare(password, hash); } // Example usage: async function registerUser(password: string) { const hashedPassword = await hashPassword(password); console.log("Hashed password:", hashedPassword); // Store the hashed password in the database } async function loginUser(password: string, hashedPasswordFromDB: string) { const passwordMatch = await comparePassword(password, hashedPasswordFromDB); if (passwordMatch) { console.log("Login Successful"); } else { console.log("Login Failed"); } } //Example calls. Never store passwords in memory like this! const examplePassword = "StrongPassword123!"; registerUser(examplePassword); //Pretend we retrieve this from the database const hashedPasswordFromDB = "$2b$10$IWD5R8X3AAvbZzV2L5l51eG99.W6eO8B5s5F4h4k9.8wzJ8eO6t2"; loginUser(examplePassword, hashedPasswordFromDB); """ Authorization middleware function for Express: """typescript import { Request, Response, NextFunction } from 'express'; interface CustomRequest extends Request { user?: { role: string }; // Extend Request type to include user info } function authorize(role: string) { return (req: CustomRequest, res: Response, next: NextFunction) => { if (!req.user || req.user.role !== role) { return res.status(403).json({ message: 'Unauthorized' }); // 403 Forbidden } next(); }; } // Usage in an Express route: // app.get('/admin', authenticate, authorize('admin'), (req: Request, res: Response) => { // res.json({ message: 'Admin access granted' }); // }); """ **Anti-Pattern:** """typescript // Vulnerable code - storing passwords in plaintext! function createUser(username: string, password: string): void { // BAD PRACTICE: Never store passwords in plain text. // Storing passwords wihout hashing is extremely dangerous. console.log("Creating user ${username} with password ${password}"); } createUser('testUser', 'insecurePassword'); """ ### Technology Specific Details * **JWT (JSON Web Tokens):** When working with JWTs, always verify the signature server-side using a secret key that is securely stored and rotated regularly. Ensure that you are using the "jsonwebtoken" library correctly to prevent tampering or replay attacks. * **OAuth 2.0:** Properly configure your OAuth 2.0 flows. Ensure redirect URIs are validated to prevent authorization code injection. Always use PKCE (Proof Key for Code Exchange) for public clients (e.g., mobile and browser-based apps). * **CORS (Cross-Origin Resource Sharing):** Configure CORS carefully. Only allow specific and known origins, and avoid wildcard settings ("*"). Implement proper preflight request handling. ## 3. Data Protection ### Standard Protect sensitive data both in transit and at rest. **Why:** Preventing data breaches and maintaining compliance with privacy regulations. **Do This:** * Use HTTPS for all network communication. * Encrypt sensitive data at rest (e.g., database fields, files). * Implement encryption algorithms that adhere to industry best practices. * Use environment variables and secrets management for sensitive credentials. **Don't Do This:** * Transmit sensitive data over unencrypted channels (HTTP). * Store sensitive data in plaintext in databases or configuration files. * Embed secrets directly in code. **Code Examples:** HTTPS enforcement in Express: """typescript import express, { Request, Response, NextFunction } from 'express'; import helmet from 'helmet'; const app = express(); // Force HTTPS using helmet app.use(helmet({ hsts: { maxAge: 31536000, // One year in seconds includeSubDomains: true, preload: true }, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:"], upgradeInsecureRequests: [], }, }, })); //Middleware to redirect HTTP to HTTPS if not already secured. Avoids infinite redirect loops in properly configured environments app.use((req: Request, res: Response, next: NextFunction) => { if (req.secure) { // request was via https, so do no special handling next(); } else { // request was via http, so redirect to https res.redirect('https://' + req.headers.host + req.url); } }); app.get('/', (req: Request, res: Response) => { res.send('Hello, world!'); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log("Server is running on port ${port}"); }); """ Example of encrypting data at rest using the "crypto" library: """typescript import crypto from 'crypto'; const algorithm = 'aes-256-cbc'; // Choose a strong encryption algorithm const key = crypto.randomBytes(32); // Generate a secure encryption key const iv = crypto.randomBytes(16); // Initialization vector /** * Encrypts text using AES-256-CBC with a provided key and IV. * @param text The text to be encrypted. * @returns An object containing the encrypted data, IV, and encryption key. In a real environment, the key would be managed independently and not be present in the return result. */ function encrypt(text: string) { const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv); let encrypted = cipher.update(text); encrypted = Buffer.concat([encrypted, cipher.final()]); return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex'), encryptionKey: key.toString('hex') }; //NEVER RETURN THE KEY AND IV LIKE THIS IN PRODUCTION. This is for demonstration purposes only } /** * Decrypts the encrypted data using the provided IV and encryption key * @param ivString The initialization vector * @param encryptedDataString The encrypted data. * @returns The decrypted text. */ function decrypt(ivString: string, encryptedDataString: string, encryptionKey: string) { const iv = Buffer.from(ivString, 'hex'); const encryptedText = Buffer.from(encryptedDataString, 'hex'); const decipher = crypto.createDecipheriv(algorithm, Buffer.from(encryptionKey, 'hex'), iv); let decrypted = decipher.update(encryptedText); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); } const text = 'Sensitive data to be encrypted'; const encryptionResult = encrypt(text); // NEVER store the encryption key and IV together with the encrypted Data. //This is for demonstration purposes only const ivReturned = encryptionResult.iv; const encryptedDataReturned = encryptionResult.encryptedData; const keyReturned = encryptionResult.encryptionKey; console.log("IV: ", ivReturned); console.log("Encrypted Data: ", encryptedDataReturned); const decryptedText = decrypt(ivReturned, encryptedDataReturned, keyReturned); console.log("Decrypted Text: ", decryptedText); //Logs "Sensitive data to be encrypted" """ **Anti-Pattern:** """typescript // Vulnerable code - storing API keys directly in the code const apiKey = 'YOUR_API_KEY'; // UNSAFE: Never store secrets directly in the source code. This is particularly dangerous if the respository is public! """ ## 4. Dependency Management ### Standard Regularly review and update dependencies, addressing known vulnerabilities. **Why:** Mitigating risks from vulnerable third-party libraries and frameworks. **Do This:** * Use a dependency management tool (e.g., npm, yarn, pnpm). * Regularly run security audits (e.g., "npm audit", "yarn audit"). * Update dependencies to the latest stable versions. * Use tools like Snyk, Dependabot, or similar that automate vulnerability scanning, dependency updates and automatically create pull requests. * Consider using a Software Bill of Materials tool (SBOM) to maintain an inventory of all software components and their dependencies. Examples tools include Syft, Grype, or CycloneDX. **Don't Do This:** * Use outdated or unmaintained dependencies. * Ignore security audit warnings. * Install dependencies from untrusted sources. **Code Examples:** Running a security audit with npm: """bash npm audit """ Updating vulnerable dependencies: """bash npm update """ Using "npm audit fix": """bash npm audit fix //Tries to automatically fix known vulnerabilities -- careful with breaking changes and test thoroughly! """ **Anti-Pattern:** """typescript // Vulnerable code - using outdated library without security updates import $ from 'jquery'; // Using an old, vulnerable version of jQuery """ ### Technology Specific Details * **Lockfiles**: Always commit your lockfiles ("package-lock.json", "yarn.lock", "pnpm-lock.yaml") to your repository. Lockfiles ensure that everyone on the team is using the exact same versions of dependencies, preventing issues caused by automatically updating minor versions. * **"npm ci" command**: Use the "npm ci" command in your CI/CD pipeline. "npm ci" installs dependencies from the "package-lock.json" file, ensuring a clean and reproducible build. It's significantly faster and more secure because it doesn't update the lock file. * **Subresource Integrity (SRI)**: When using CDNs, use SRI to ensure that the files you load from the CDN have not been tampered with. Generate the SRI hash and include it in your "<script>" or "<link>" tags. Example: "<script src="https://example.com/script.js" integrity="sha384-..." crossorigin="anonymous"></script>" * **Scope your packages**: Use npm's scoped packages to prevent naming conflicts and increase security. Scopes are particularly important for proprietary or private packages. * **Consider supply chain security**: Investigate and implement measures to prevent supply chain attacks, such as verifying package authenticity and using a private npm registry. ## 5. Error Handling and Logging ### Standard Implement proper error handling and logging mechanisms, avoiding information leaks. **Why:** Preventing sensitive information from being exposed in error messages or logs. **Do This:** * Implement centralized error handling. * Log errors and exceptions, but redact sensitive data. * Use structured logging for easier analysis. * Monitor logs for suspicious activity. **Don't Do This:** * Expose stack traces or sensitive data in error messages to end users. * Log sensitive data in production logs. * Ignore errors and exceptions. Catch and handle exceptions using try-catch blocks. **Code Examples:** Centralized error handling in Express: """typescript import express, { Request, Response, NextFunction } from 'express'; const app = express(); app.get('/', (req: Request, res: Response, next: NextFunction) => { try { // Simulate an error throw new Error('Simulated error'); } catch (error) { next(error); // Pass error to error handling middleware } }); // Error handling middleware app.use((err: any, req: Request, res: Response, next: NextFunction) => { console.error(err.stack); // Log the error stack trace (redact sensitive data) res.status(500).send('Something went wrong!'); // Generic error message to the user }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); """ Structured logging with Winston: """typescript import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.json(), defaultMeta: { service: 'user-service' }, transports: [ new winston.transports.Console({ format: winston.format.simple(), }), new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); // Example usage: // logger.info('User logged in', { userId: 123 }); // logger.error('Failed to process payment', { error: 'Invalid card', userId: 123 }); """ **Anti-Pattern:** """typescript // Vulnerable code - exposing stack traces to the user app.use((err: any, req: Request, res: Response, next: NextFunction) => { // BAD PRACTICE: Exposing full stack traces to users can reveal sensitive information about the server's internal workings. Don't do this in production. res.status(500).send(err.stack); }); """ ## 6. Code Reviews and Static Analysis ### Standard Conduct thorough code reviews and use static analysis tools to identify potential vulnerabilities. **Why:** Detecting security issues early in the development lifecycle. **Do This:** * Implement a code review process involving multiple developers. * Use static analysis tools to automatically scan for security vulnerabilities. * Integrate security checks into the CI/CD pipeline. **Don't Do This:** * Skip code reviews. * Ignore static analysis warnings. * Deploy code without security checks. **Code Examples:** Example using ESLint with security-related plugins: """javascript // .eslintrc.js module.exports = { "plugins": [ "security", "no-secrets" ], "rules": { "security/detect-unsafe-regex": "warn", "security/detect-eval-with-expression": "warn", "security/detect-pseudoRandomBytes": "warn", "no-secrets/no-secrets": "error" } }; """ Run ESLint with the security plugin: """bash eslint . """ Example with SonarQube """bash // Perform a SonarQube scan sonar-scanner -Dsonar.projectKey=my-project -Dsonar.sources=. -Dsonar.host.url=http://localhost:9000 -Dsonar.login=your_token """ **Anti-Pattern:** """typescript // Vulnerable code - deploying code without review or static analysis function deployCode(code: string): void { // Unsafe: Deploying code without proper review can lead to vulnerabilities console.log('Deploying code:', code); } deployCode('Unsafe Code'); """ ## 7. Session Management ### Standard Implement secure session management to prevent session hijacking and fixation attacks. **Why:** Protecting user sessions from unauthorized access. **Do This:** * Use cryptographically secure session IDs. * Rotate session IDs after login. * Set appropriate session timeouts. * Store session data securely. **Don't Do This:** * Use predictable session IDs. * Store sensitive data in session cookies. * Use long session timeouts without appropriate measures. **Code Examples:** Session management with Express and "express-session": """typescript import express, { Request, Response } from 'express'; import session from 'express-session'; import crypto from 'crypto'; const app = express(); // Generate a random secret for session encryption. In a real environment, this would be securely stored and rotated regularly. const sessionSecret = crypto.randomBytes(32).toString('hex'); app.use(session({ secret: sessionSecret, // Use a strong, randomly generated secret resave: false, saveUninitialized: false, cookie: { secure: true, // Serve only over HTTPS in production httpOnly: true, // Prevent client-side JavaScript access maxAge: 60 * 60 * 1000 // Session timeout of 1 hour } })); app.get('/', (req: Request, res: Response) => { req.session.views = (req.session.views || 0) + 1; res.send("You have viewed this page ${req.session.views} times"); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); """ Rotating session ID after login: """typescript app.post('/login', (req: Request, res: Response) => { // Validate user credentials if (isValidUser(req.body.username, req.body.password)) { req.session.regenerate((err) => { // Regenerate session ID after successful login if (err) { console.log(err); } req.session.user = { username: req.body.username }; res.send('Login successful'); }); } else { res.status(401).send('Invalid credentials'); } }); """ **Anti-Pattern:** """typescript // Vulnerable code - using a weak session secret app.use(session({ secret: 'insecureSecret', // BAD: Use a strong, randomly generated secret. hardcoded secrets shouldn't be used. resave: false, saveUninitialized: true })); """ ## 8. Cross-Site Scripting (XSS) Prevention ### Standard Employ techniques to prevent XSS attacks, such as output encoding and Content Security Policy (CSP). **Why:** Preventing malicious scripts from being injected into web pages. **Do This:** * Encode output based on context (HTML, JavaScript, URL). * Use a templating engine that automatically handles output encoding. * Implement Content Security Policy (CSP) to restrict the sources of content. **Don't Do This:** * Insert user-controlled data directly into HTML without encoding. * Disable CSP without a strong justification. **Code Examples:** Output encoding using a templating engine (e.g., Handlebars): """html // Handlebars template with automatic output encoding. Most modern template systems like React, Angular, Vue, Jinja, and others employ measures to guard against XSS by default. <p>Hello, {{name}}!</p> """ Implementing CSP using Helmet: """typescript import express from 'express'; import helmet from 'helmet'; const app = express(); app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:"], upgradeInsecureRequests: [], }, })); app.get('/', (req, res) => { res.send('Hello, world!'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); """ **Anti-Pattern:** """typescript // Vulnerable code - inserting user input directly into HTML function displayUserInput(input: string): void { document.getElementById('output').innerHTML = input; // Vulnerable to XSS } """ ## 9. Cross-Site Request Forgery (CSRF) Prevention ### Standard Implement CSRF protection mechanisms to prevent malicious websites from performing unauthorized actions on behalf of authenticated users. **Why:** Protecting against unauthorized actions triggered by cross-site requests. **Do This:** * Use anti-CSRF tokens for all state-changing requests (e.g., POST, PUT, DELETE). * Validate the Origin or Referer header on the server-side. * Use the SameSite cookie attribute to mitigate CSRF attacks. **Don't Do This:** * Rely solely on cookies for authentication, without CSRF protection. * Disable CSRF protection without understanding the risks. **Code Examples:** CSRF protection with Express and "csurf": """typescript import express, { Request, Response } from 'express'; import csurf from 'csurf'; import cookieParser from 'cookie-parser'; const app = express(); app.use(cookieParser()); const csrfProtection = csurf({ cookie: true }); app.use(csrfProtection); app.get('/form', (req: Request, res: Response) => { // pass the csrfToken to the view res.send(" <form action="/process" method="POST"> <input type="hidden" name="_csrf" value="${req.csrfToken()}"> <button type="submit">Submit</button> </form> "); }); app.post('/process', csrfProtection, (req: Request, res: Response) => { res.send('Form processed successfully!'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); """ **Anti-Pattern:** """typescript // Vulnerable code - not using CSRF protection app.post('/transfer', (req: Request, res: Response) => { // Perform money transfer without CSRF protection // This is vulnerable to CSRF attacks }); """ ## 10. Rate Limiting ### Standard Implement rate limiting to protect against brute-force attacks and resource exhaustion. **Why:** Preventing abuse of resources and maintaining service availability. **Do This:** * Implement rate limiting middleware. * Configure rate limits based on the sensitivity of the endpoint. * Use different rate limits for authenticated and unauthenticated users. **Don't Do This:** * Expose endpoints without any rate limiting. * Set overly generous rate limits. **Code Examples:** Rate limiting with Express and "express-rate-limit": """typescript import express from 'express'; import rateLimit from 'express-rate-limit'; const app = express(); const limiter = rateLimit({ 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 after 15 minutes', standardHeaders: true, // Return rate limit info in the "RateLimit-*" headers legacyHeaders: false, // Disable the "X-RateLimit-*" headers }); // Apply the rate limiting middleware to all requests app.use(limiter); app.get('/', (req, res) => { res.send('Hello, world!'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); """ **Anti-Pattern:** """typescript // Vulnerable code - no rate limiting app.post('/login', (req: Request, res: Response) => { // Process login attempt without rate limiting // This is vulnerable to brute-force attacks }); """ ## 11. TypeScript Specific Security Considerations ### Standard Leverage TypeScript features to enhance security and prevent common errors. **Why:** TypeScript's strong typing system and compile-time checks can help catch vulnerabilities early. Enums, Readonly properties and interfaces should be leveraged for security of systems. **Do This:** * Use strict mode (""strict": true" in "tsconfig.json"). * Utilize "readonly" to prevent accidental modification of sensitive data. * Define clear interfaces to enforce data structures. * Use type guards to narrow types and prevent unexpected behavior. * Prefer "enum" over string literals for known, finite sets of values. **Don't Do This:** * Disable strict mode. * Rely on "any" type excessively. * Ignore TypeScript compiler errors. **Code Examples:** Using "readonly" for preventing data modification: """typescript interface User { readonly id: string; name: string; } const user: User = { id: '123', name: 'John Doe', }; // user.id = '456'; // Compile-time error: Cannot assign to 'id' because it is a read-only property. user.name = 'Jane Doe'; // Allowed """ ### Technology Specific Details * **"--noImplicitAny"**: Always enable the "--noImplicitAny" compiler option in your "tsconfig.json". This flag ensures that you explicitly type every variable, reducing the risk of unexpected "any" types creeping into your code. * **"--strictNullChecks"**: Enable "--strictNullChecks" to prevent null and undefined errors. This helps catch potential runtime exceptions early. * **Use discriminated unions**: Discriminated unions(tagged unions) are useful for creating safer and more predictable code. They force you to handle all possible cases, reducing the risk of runtime errors. * **Consider using Zod or Yup**: These libraries allow you to define schemas and validate data at runtime, providing an extra layer of security against unexpected data. **Anti-Pattern:** """typescript // Vulnerable code - using "any" type excessively function processData(data: any): void { console.log(data.someProperty); // No type checking, potential runtime error } """ These standards provide a foundation for developing secure TypeScript applications. Regular training, code reviews, and updates to these standards are crucial to adapt to evolving security threats.
# Tooling and Ecosystem Standards for TypeScript This document outlines the recommended tooling, libraries, and ecosystem practices for TypeScript development, focusing on maintainability, performance, and security within modern TypeScript projects. ## 1. Project Setup and Configuration ### 1.1. "tsconfig.json" Configuration **Standard:** Use a well-defined "tsconfig.json" with strict compiler options. **Why:** A strict "tsconfig.json" helps catch errors early, enforces best practices, and improves code quality. **Do This:** """json { "compilerOptions": { "target": "es2022", "module": "esnext", "moduleResolution": "node", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "resolveJsonModule": true, "sourceMap": true, "outDir": "dist", "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } """ * "target": Specifies the ECMAScript target version. Use "es2022" or later to leverage modern features. * "module": Specifies the module system. "esnext" is excellent for modern bundlers. * "moduleResolution": "node" is standard for Node.js-style module resolution. * "esModuleInterop": Enables interoperability between CommonJS and ES modules. * "forceConsistentCasingInFileNames": Prevents case-sensitive import issues. Crucial for collaboration. * "strict": Enables all strict type-checking options for maximum safety. * "skipLibCheck": Improves build times by skipping type checking for declaration files. * "resolveJsonModule": Allows importing ".json" files as modules. Useful for configuration. * "sourceMap": Generates source map files for debugging. * "outDir": Specifies the output directory for compiled JavaScript files. * "baseUrl" and "paths": Configure module path aliases to reduce relative import verbosity, especially for large projects. **Don't Do This:** * Disabling "strict" mode, as it bypasses critical type checks. * Using older "target" versions (e.g., "es5") unnecessarily. * Omitting "esModuleInterop", leading to potential runtime errors when mixing module types. * Using inconsistent casing in file names, which breaks imports on case-sensitive file systems. ### 1.2. Linting (ESLint) **Standard:** Integrate ESLint with a TypeScript-specific configuration. **Why:** Linting enforces code style, identifies potential bugs, and promotes consistency across the codebase. **Do This:** 1. Install ESLint and related plugins: """bash npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev """ 2. Configure ESLint (".eslintrc.js" or ".eslintrc.json"): """javascript module.exports = { parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: 2022, sourceType: "module", project: './tsconfig.json', //Required for projects that use type-aware linting rules. }, plugins: ["@typescript-eslint"], extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", ], rules: { // Add your custom rules here "@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/no-explicit-any": "warn", "no-unused-vars": "off", //Handled by typescript/eslint "@typescript-eslint/no-unused-vars": ["warn"], "@typescript-eslint/no-floating-promises": "warn", }, ignorePatterns: ['.eslintrc.js'], }; """ * "parser": Specifies the TypeScript parser. * "plugins": Includes the TypeScript ESLint plugin. * "extends": Extends recommended ESLint rules and TypeScript-specific rules. "recommended-requiring-type-checking" enables rules requiring type information. * "rules": Customizes ESLint rules. Example: "@typescript-eslint/explicit-function-return-type": Requires explicit return types for functions. "@typescript-eslint/no-floating-promises": prevents unhandled promises. * "ignorePatterns": Ignores files and directories, like the ESLint configuration file itself. 3. Add an npm script to your "package.json": """json "scripts": { "lint": "eslint src/**/*.ts" } """ **Don't Do This:** * Ignoring ESLint warnings, as they often indicate potential problems. * Using outdated ESLint configurations that do not support modern TypeScript syntax. * Disabling too many ESLint rules, weakening the code quality checks. ### 1.3. Formatting (Prettier) **Standard:** Use Prettier for consistent code formatting. **Why:** Prettier automates code formatting, ensuring a consistent style across the project and reducing debates about formatting preferences. Integrate with ESLint. **Do This:** 1. Install Prettier: """bash npm install prettier --save-dev """ 2. Create a ".prettierrc.js" or ".prettierrc.json" file: """javascript module.exports = { semi: true, trailingComma: "all", singleQuote: true, printWidth: 120, tabWidth: 2, }; """ * "semi": Adds semicolons at the end of statements. * "trailingComma": Adds trailing commas in arrays, objects, and function parameters for cleaner diffs. * "singleQuote": Uses single quotes for strings. * "printWidth": Specifies the maximum line length. * "tabWidth": Specifies the number of spaces per indentation level. 3. Integrate with ESLint: """bash npm install eslint-config-prettier eslint-plugin-prettier --save-dev """ Modify your ".eslintrc.js" to extend "prettier" and add the "prettier" plugin: """javascript module.exports = { //... other configuration extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "prettier" ], plugins: ["@typescript-eslint", "prettier"], rules: { // Add your custom rules here "prettier/prettier": "error" }, }; """ 4. Add an npm script to your "package.json": """json "scripts": { "format": "prettier --write src/**/*.ts" } """ **Don't Do This:** * Skipping Prettier integration, leading to inconsistent formatting. * Using conflicting ESLint and Prettier rules, creating formatting conflicts. * Ignoring Prettier warnings, as they indicate formatting issues or errors. ### 1.4 Package Management **Standard** Use npm or yarn for package management. Opt for yarn if deterministic installs and workspace features are needed. **Why**: Package managers handle dependencies and ensure consistent project build environments. **Do this**: * Always commit "package-lock.json" (npm) or "yarn.lock" (yarn) files to version control. * Use semantic versioning (semver) for dependencies. * Use dependency ranges (e.g., "^1.2.3") to allow minor and patch updates. * Regularly update dependencies to benefit from bug fixes and new features. """json "dependencies": { "lodash": "^4.17.21", "react": "^18.2.0" }, "devDependencies": { "@types/lodash": "^4.14.191", "@types/react": "^18.0.26" } """ **Don't Do This:** * Committing the "node_modules" directory to version control. * Using wildcard ("*") versions for dependencies, as this can lead to unpredictable builds. * Ignoring security vulnerabilities reported by package managers. * Not specifying a "engines" property in "package.json" especially if server-side rendering or specific Node versions are required. ## 2. Recommended Libraries and Tools ### 2.1. State Management **Standard:** Choose a state management library based on project needs. * **Redux:** For complex applications with predictable state mutations. Consider using Redux Toolkit for simplified setup. * **Zustand:** Simpler, bear-bones solution for simpler use cases. * **Recoil/Jotai:** For React applications leveraging atom-based state management. * **Context API + "useReducer":** Suitable for smaller applications or local component state management. **Why:** A well-chosen state management solution improves code organization, simplifies data flow, and enhances maintainability. **Do This (Redux Toolkit example):** 1. Install Redux Toolkit and React Redux: """bash npm install @reduxjs/toolkit react-redux """ 2. Define a slice: """typescript import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface CounterState { value: number; } const initialState: CounterState = { value: 0, }; const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; """ 3. Configure the store: """typescript import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; const store = configureStore({ reducer: { counter: counterReducer, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export default store; """ 4. Provide the store in your React application: """tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { Provider } from 'react-redux'; import store from './store'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> ); """ **Don't Do This:** * Using global variables for state management, leading to unpredictable state mutations. * Choosing a state management solution that is overly complex for the project's needs. * Mutating state directly in Redux reducers (use Redux Toolkit or Immer). ### 2.2. HTTP Clients **Standard:** Use "axios" or "fetch" with proper error handling for making HTTP requests. Consider libraries like "tanstack/react-query" or "swr" for data fetching and caching. **Why:** Reliable HTTP clients ensure efficient data fetching, handle errors gracefully, and improve application performance. **Do This ("axios" example):** 1. Install "axios": """bash npm install axios """ 2. Make a request: """typescript import axios from 'axios'; interface Post { userId: number; id: number; title: string; body: string; } async function getPosts(): Promise<Post[]> { try { const response = await axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts'); return response.data; } catch (error) { console.error('Error fetching posts:', error); throw error; } } """ 3. Using "tanstack/react-query" """typescript import { useQuery } from '@tanstack/react-query' interface Post { userId: number; id: number; title: string; body: string; } const fetchPosts = async (): Promise<Post[]> => { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; function usePosts() { return useQuery<Post[], Error>(['posts'], fetchPosts); } export default usePosts; """ **Don't Do This:** * Using "XMLHttpRequest" directly, as it is verbose and lacks modern features. * Ignoring error handling when making HTTP requests, leading to unhandled exceptions. * Hardcoding API URLs in the code. ### 2.3. Date/Time Manipulation **Standard:** Use "date-fns" or "dayjs" for date/time manipulation. **Why:** These libraries provide a consistent and reliable API for date/time operations, avoiding the pitfalls of the native JavaScript "Date" object. **Do This ("date-fns" example):** 1. Install "date-fns": """bash npm install date-fns """ 2. Format a date: """typescript import { format, subDays } from 'date-fns'; const today = new Date(); const yesterday = subDays(today, 1); const formattedDate = format(yesterday, 'yyyy-MM-dd'); console.log(formattedDate); """ *Uses consistent and easy to understand api surface.* **Don't Do This:** * Using the native JavaScript "Date" object directly, as it is prone to errors and inconsistencies. * Rolling your own date/time manipulation functions, reinventing the wheel. ### 2.4 Utility Libraries **Standard:** Use "lodash" or "ramda" for common utility functions. **Why:** These libraries provide optimized and well-tested utility functions for common tasks like array manipulation, object merging. **Do This ("lodash" example):** 1. Install "lodash": """bash npm install lodash """ 2. Use a utility function: """typescript import * as _ from 'lodash'; const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; const userIds = _.map(users, 'id'); console.log(userIds); // Output: [1, 2] """ *Lodash provides numerous tested utilities, prevent simple re-inventions and potential unhandled edge cases.* **Don't Do This:** * Reimplementing common utility functions that are already available in these libraries. ### 2.5 Testing **Standard:** Use Jest or Vitest for unit testing and Playwright or Cypress for end-to-end testing. **Why:** Testing ensures code quality, prevents regressions, and improves maintainability. **Do This (Jest example):** 1. Install Jest and related dependencies: """bash npm install jest @types/jest ts-jest --save-dev """ 2. Configure Jest in "jest.config.js": """javascript /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; """ 3. Write a test: """typescript // sum.ts export function sum(a: number, b: number): number { return a + b; } // sum.test.ts import { sum } from './sum'; describe('sum', () => { it('should add two numbers correctly', () => { expect(sum(1, 2)).toBe(3); }); }); """ 4. Add an npm script to your "package.json": """json "scripts": { "test": "jest" } """ **Don't Do This:** * Skipping testing, leading to increased risk of bugs and regressions. * Writing overly complex tests that are difficult to maintain. * Testing implementation details rather than behavior. ### 2.6 Documentation **Standard:** Use JSDoc or TypeDoc for generating API documentation. **Why:** Clear documentation makes it easier for developers to understand and use the codebase. **Do This (TypeDoc example):** 1. Install TypeDoc: """bash npm install typedoc --save-dev """ 2. Add JSDoc-style comments to your code: """typescript /** * Adds two numbers together. * * @param a The first number. * @param b The second number. * @returns The sum of a and b. */ export function sum(a: number, b: number): number { return a + b; } """ 3. Generate documentation using TypeDoc: """bash typedoc --out docs src """ **Don't Do This:** * Writing incomplete or outdated documentation. * Omitting type information in documentation. ## 3. Modern Approaches & Patterns ### 3.1. Monorepos **Standard:** Consider using a monorepo for large projects with multiple packages or applications. **Why:** Monorepos simplify code sharing, improve dependency management, and enable atomic changes across multiple packages. **Do This:** * Use tools like Nx or Lerna to manage monorepos. * Structure the monorepo with clear package boundaries. * Use workspace features in npm or yarn to optimize dependency installation. * Utilize Nx's build caching and task orchestration to speed up builds. ### 3.2. Serverless Functions **Standard:** Use TypeScript for writing serverless functions. **Why:** TypeScript's type safety and tooling improve the development experience for serverless functions, reducing the risk of runtime errors. **Do This:** * Use frameworks like Serverless Framework or AWS CDK to deploy serverless functions. * Use dependency injection to improve testability. * Implement proper error handling and logging. * Type your event handlers as specifically as possible. """typescript import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { try { const name = event.queryStringParameters?.name || 'World'; const message = "Hello, ${name}!"; return { statusCode: 200, body: JSON.stringify({ message }), }; } catch (error) { console.error(error); return { statusCode: 500, body: JSON.stringify({ message: 'Internal Server Error' }), }; } }; """ ### 3.3 Module Federation **Standard** For large applications that are composed from multiple teams and deployments, consider Module Federation. **Why:** Module Federation promotes code reuse, team autonomy, and incremental migrations to newer tech stacks. **Do This:** * Use tools like Webpack 5 to setup Module Federation. * Define clear interfaces and boundaries between federated modules. * Establish versioning and compatibility standards for shared modules. """typescript // webpack.config.js example for consuming a remote module in React const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { // Existing config plugins: [ new ModuleFederationPlugin({ name: 'Host', // Name of the local application (Host) remotes: { // Mapping of remote app name to remote entry point 'RemoteApp': 'RemoteApp@http://localhost:3001/remoteEntry.js', }, shared: ['react', 'react-dom'], // Shared dependencies }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], }; """ This allows you to consume React components from RemoteApp as if they were local using dynamic imports or React.lazy: """javascript // Example of consuming a dynamic remote component import React, { Suspense } from 'react'; const RemoteComponent = React.lazy(() => import('RemoteApp/Component')); const App = () => { return ( <div> <h1>Host Application</h1> <Suspense fallback={<div>Loading...</div>}> <RemoteComponent /> </Suspense> </div> ); }; export default App; """ ## 4. Security Best Practices ### 4.1. Dependency Security **Standard:** Regularly audit dependencies for security vulnerabilities. **Why:** Dependencies can contain security vulnerabilities that can compromise the application. **Do This:** * Use "npm audit" or "yarn audit" to identify vulnerabilities. * Update vulnerable dependencies to the latest versions. * Use tools like Snyk to automate dependency vulnerability scanning. ### 4.2. Input Validation **Standard:** Validate all user inputs to prevent injection attacks. **Why:** Input validation prevents attackers from injecting malicious code or data into the application. **Do This:** * Use schema validation libraries like Zod or Yup to validate inputs. * Sanitize inputs to remove or escape potentially harmful characters. """typescript import { z } from 'zod'; const userSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), }); function createUser(data: unknown) { const result = userSchema.safeParse(data); if (!result.success) { console.error('Validation error:', result.error); return null; } return result.data; } """ ### 4.3. Secret Management **Standard:** Store secrets securely and avoid hardcoding them in the code. **Why:** Hardcoding secrets exposes them to potential attackers. **Do This:** * Use environment variables to store secrets. * Use tools like HashiCorp Vault or AWS Secrets Manager to manage secrets. * Never commit secrets to version control. ### 4.4. Rate Limiting **Standard:** implement rate limiting to prevent abuse and denial-of-service attacks. **Why:** Prevent resource exhaustion and malicious activities by limiting the number of requests from a single source within a given timeframe. **Do This:** * Use middleware to throttle requests based on IP address, user ID, or other identifying factors. * Implement retry mechanisms with exponential backoff for clients to handle rate limits gracefully. * Monitor rate limiting metrics to identify potential attacks or misconfigured clients. """typescript // Express middleware example using a simple in-memory store import rateLimit from 'express-rate-limit'; import express, { Request, Response } from 'express'; const app = express(); const limiter = rateLimit({ 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 after 15 minutes', standardHeaders: true, // Return rate limit info in the "RateLimit-*" headers legacyHeaders: false, // Disable the "X-RateLimit-*" headers }); // Apply the rate limiting middleware to all requests app.use(limiter); app.get('/', (req: Request, res: Response) => { res.send('Hello World!'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); }); """ This concludes the Tooling and Ecosystem standards document for TypeScript. By following these guidelines, development teams can build maintainable, secure, and performant TypeScript applications.
# API Integration Standards for TypeScript This document outlines the standards and best practices for integrating with backend services and external APIs in TypeScript projects. Adhering to these guidelines will result in more maintainable, performant, and secure code. ## 1. Architectural Considerations ### 1.1. Abstraction and Decoupling **Standard:** Decouple API interaction logic from core application logic using abstraction layers. **Why:** This separates concerns, improves testability, reduces dependencies, and allows for easier switching of API implementations without affecting the rest of the application. **Do This:** """typescript // api-client.ts export interface APIClient { fetchData(endpoint: string): Promise<any>; } export class DefaultAPIClient implements APIClient { async fetchData(endpoint: string): Promise<any> { const response = await fetch(endpoint); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } // service.ts import { APIClient, DefaultAPIClient } from './api-client'; export class MyService { private apiClient: APIClient; constructor(apiClient: APIClient = new DefaultAPIClient()) { this.apiClient = apiClient; } async getData(): Promise<any> { return this.apiClient.fetchData('/api/data'); } } """ **Don't Do This:** """typescript // service.ts (Anti-pattern: Tightly coupled API call) export class MyService { async getData(): Promise<any> { const response = await fetch('/api/data'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } """ ### 1.2. Centralized API Configuration **Standard:** Store API endpoints, timeouts, and authentication details in a centralized configuration. **Why:** Easier management of API configurations across the application, enabling quick updates and environment-specific configurations. Leveraging environment variables is crucial. **Do This:** """typescript // config.ts export const API_CONFIG = { baseURL: process.env.REACT_APP_API_BASE_URL || 'https://api.example.com', timeout: 5000, // milliseconds apiKey: process.env.REACT_APP_API_KEY, }; // api-client.ts import { API_CONFIG } from './config'; export class DefaultAPIClient { async fetchData(endpoint: string): Promise<any> { try { const response = await fetch("${API_CONFIG.baseURL}${endpoint}", { headers: { 'x-api-key': API_CONFIG.apiKey || '' }, signal: AbortSignal.timeout(API_CONFIG.timeout) }); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } catch (error) { console.error("API Fetch Error:", error); throw error; // Re-throw to allow calling code to handle the error } } } """ **Don't Do This:** """typescript // api-client.ts (Anti-pattern: Hardcoded API endpoint) export class DefaultAPIClient { async fetchData(): Promise<any> { const response = await fetch('https://api.example.com/api/data'); // Hardcoded URL if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } """ ### 1.3. Error Handling Strategy **Standard:** Implement a consistent and centralized error handling mechanism for API calls. Use try/catch blocks and custom error classes. **Why:** Provides a predictable way to handle API failures, improving debugging and enhancing user experience (e.g., showing appropriate error messages). **Do This:** """typescript // api-client.ts export class APIError extends Error { constructor(message: string, public statusCode: number) { super(message); this.name = 'APIError'; } } export class DefaultAPIClient { async fetchData(endpoint: string): Promise<any> { try { const response = await fetch(endpoint); if (!response.ok) { throw new APIError("HTTP error! status: ${response.status}", response.status); } return await response.json(); } catch (error: any) { // Explicit 'any' type for error // Log the error, potentially with more context console.error('API call failed:', error); throw error; // Re-throw to propagate the error } } } // service.ts import { APIError } from './api-client'; export class MyService { async getData(): Promise<any> { try { return await this.apiClient.fetchData('/api/data'); } catch (error) { if (error instanceof APIError) { console.error("API Error: ${error.message}, Status Code: ${error.statusCode}"); // Handle specific API errors (e.g., display user-friendly message) } else { console.error('Unexpected error:', error); // Handle unexpected errors } throw error; // Re-throw or handle as needed } } } """ **Don't Do This:** """typescript // api-client.ts (Anti-pattern: Ignoring errors) export class DefaultAPIClient { async fetchData(endpoint: string): Promise<any> { try { const response = await fetch(endpoint); return await response.json(); // No error checking, potential crash } catch (error) { console.error("Error fetching data"); //Logging, but program continues as if nothing happened return null; //This is a very bad practice } } } """ ### 1.4. Data Transformation **Standard:** Implement data transformation and mapping logic to adapt API responses to the application's data models. **Why:** Decouples the application from API-specific data structures, improving flexibility and maintainability. **Do This:** """typescript // api-client.ts export interface RawUserData { id: number; firstName: string; lastName: string; emailAddress: string; } // models.ts export interface User { id: number; fullName: string; email: string; } // api-client.ts export class DefaultAPIClient { async fetchData(endpoint: string): Promise<RawUserData[]> { const response = await fetch(endpoint); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } // service.ts import {DefaultAPIClient, RawUserData} from './api-client'; import {User} from './models'; export class UserService { constructor(private apiClient: DefaultAPIClient) {} async getUsers(): Promise<User[]> { const rawUsers: RawUserData[] = await this.apiClient.fetchData('/api/users'); return rawUsers.map(this.transformUser); } private transformUser(rawUser: RawUserData): User { return { id: rawUser.id, fullName: "${rawUser.firstName} ${rawUser.lastName}", email: rawUser.emailAddress, }; } } """ **Don't Do This:** """typescript // service.ts (Anti-pattern: Direct use of API data structures) interface RawUserData { //Defined inside the service; tight coupling id: number; firstName: string; lastName: string; emailAddress: string; } export class MyService { async getData(): Promise<RawUserData[]> { const response = await await fetch('/api/users'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } """ ## 2. Implementation Details ### 2.1. Using "fetch" API **Standard:** Use the native "fetch" API or a library built on top of it (e.g., "axios") for making HTTP requests. Configure request timeouts and handle abort signals. **Why:** "fetch" is the standard API for making web requests, is supported natively in modern browsers/Node.js, and offers a cleaner interface compared to older methods like "XMLHttpRequest". **Do This:** """typescript // api-client.ts import { API_CONFIG } from './config'; export class DefaultAPIClient { async fetchData(endpoint: string): Promise<any> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.timeout); try { const response = await fetch("${API_CONFIG.baseURL}${endpoint}", { signal: controller.signal, // Pass the abort signal }); clearTimeout(timeoutId); // Clear timeout if request completes if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } catch (error: any) { clearTimeout(timeoutId); //clear timeout if abort happens console.error('Fetch error:', error); if (error.name === 'AbortError') { console.log('Request aborted due to timeout.'); } throw error; } } } """ **Don't Do This:** """typescript // api-client.ts (Anti-pattern: Using deprecated XMLHttpRequest) export class DefaultAPIClient { fetchData(endpoint: string): Promise<any> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', endpoint); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error("Request failed with status: ${xhr.status}")); } }; xhr.onerror = () => reject(new Error('Network error')); xhr.send(); }); } } """ ### 2.2. Data Serialization and Deserialization **Standard:** Use "JSON.stringify" and "JSON.parse" for serializing and deserializing data when interacting with JSON APIs. Consider using libraries like "class-transformer" for complex object mapping. **Why:** Ensures proper data formatting during API interactions. Advanced libraries provide features like type validation and custom transformation logic. **Do This:** """typescript // Example with JSON.stringify and JSON.parse interface UserData { name: string; age: number; } async function postData(url: string, data: UserData): Promise<any> { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); // Assuming the response is also JSON } """ For more complex cases, consider "class-transformer" (install with "npm install class-transformer class-validator --save") """typescript import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; class User { id: number; firstName: string; lastName: string; email: string; } async function fetchUser(url: string): Promise<User> { const response = await fetch(url); const data = await response.json(); const user = plainToClass(User, data); const errors = await validate(user); //Needs decorators on the class if (errors.length > 0) { console.error("Validation failed:", errors); throw new Error("User data validation failed"); } return user; } """ **Don't Do This:** """typescript // Anti-pattern: Manually constructing JSON strings (error-prone) const data = { name: 'John', age: 30 }; const body = '{ "name": "' + data.name + '", "age": ' + data.age + ' }'; // Avoid this! """ ### 2.3. Handling Authentication and Authorization **Standard:** Implement secure authentication and authorization mechanisms (e.g., JWT, OAuth 2.0) for accessing protected APIs. Store credentials safely (e.g., using environment variables or secure storage). Use HTTPS for all API communications. **Why:** Protects sensitive data and ensures that only authorized users/applications can access specific resources. **Do This:** """typescript // Example using JWT (JSON Web Token) in API client import { API_CONFIG } from './config'; export class DefaultAPIClient { private authToken: string | null = null; setAuthToken(token: string): void { this.authToken = token; } async fetchData(endpoint: string): Promise<any> { const headers: HeadersInit = { 'Content-Type': 'application/json', // Add authentication token if available }; if (this.authToken) { headers['Authorization'] = "Bearer ${this.authToken}"; } const response = await fetch("${API_CONFIG.baseURL}${endpoint}", { headers, }); if (!response.ok) { if (response.status === 401) { // Handle unauthorized error (e.g., redirect to login) console.error("Unauthorized access, redirecting to login"); //Example: window.location.href = '/login'; } throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } //Example usage by service: export class AuthService { constructor(private apiClient: DefaultAPIClient){} async login(credentials: Credentials): Promise<boolean> { const response = await fetch('/api/login', { method: "POST", body: JSON.stringify(credentials) }); const body = await response.json(); if(response.ok && body.token){ this.apiClient.setAuthToken(body.token); return true; } return false; } } """ **Don't Do This:** """typescript // Anti-pattern: Storing API keys directly in the code (security risk) const apiKey = 'YOUR_API_KEY'; //Never do this! """ ### 2.4. Caching Strategies **Standard:** Implement caching mechanisms to reduce the number of API calls and improve performance (e.g., using browser caching, service worker caching, or server-side caching). **Why:** Reduces latency and improves responsiveness by serving data from cache instead of making frequent API requests. **Do This (browser caching):** """typescript // Setting cache headers on the server (Node.js example) import express, { Request, Response } from 'express'; const app = express(); app.get('/api/data', (req: Request, res: Response) => { // Set cache headers res.set('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour res.json({ data: 'Cached data' }); }); """ **Do This (Service Worker caching):** This example shows a simple caching strategy within a service worker to cache API responses. This service worker would need to be setup within your application. """typescript //In service-worker.js: const CACHE_NAME = 'api-cache-v1'; const API_URL_REGEX = /^\/api\//; // Regex for API endpoints self.addEventListener('install', (event: any) => { console.log("Installing service worker"); event.waitUntil( caches.open(CACHE_NAME).then(cache => { //Cache static assets (optional) return cache.addAll(['/', '/index.html', '/styles.css', '/script.js']); }) ); }); self.addEventListener('fetch', (event: any) => { const url = new URL(event.request.url); if (url.origin === location.origin && API_URL_REGEX.test(url.pathname)) { event.respondWith( caches.match(event.request).then(response => { //Return cached response if available if(response) { console.log("Returning cached response for:", event.request.url); return response; } //Otherwise fetch from network, cache, and return return fetch(event.request).then(networkResponse => { if(!networkResponse || networkResponse.status !==200 || networkResponse.type !== 'basic'){ return networkResponse; //Don't cache if not a "good" response } const responseToCache = networkResponse.clone(); //Clone as response body can only be read once caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) ); } }); """ **Don't Do This:** """typescript // Anti-pattern: Never caching API responses (performance bottleneck) """ ### 2.5. Rate Limiting **Standard:** Implement rate limiting on the client-side to prevent overwhelming APIs with excessive requests. **Why:** Avoids exceeding API usage limits and ensures fair access to resources, improving overall application stability. **Do This:** """typescript // Example using a basic rate limiter class RateLimiter { private calls: number = 0; private lastReset: number = Date.now(); private maxCalls: number = 10; private resetInterval: number = 60000; // 1 minute canMakeCall(): boolean { const now = Date.now(); if (now - this.lastReset > this.resetInterval) { this.calls = 0; this.lastReset = now; } return this.calls < this.maxCalls; } recordCall(): void { this.calls++; } } const limiter = new RateLimiter(); async function fetchDataWithRateLimit(url: string): Promise<any> { if (limiter.canMakeCall()) { limiter.recordCall(); const response = await fetch(url); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } else { throw new Error('Rate limit exceeded. Please try again later.'); } } // Usage fetchDataWithRateLimit('/api/data') .then(data => console.log(data)) .catch(error => console.error(error)); """ **Don't Do This:** """typescript // Anti-pattern: Making API calls in rapid succession without any rate limiting """ ## 3. Asynchronous Operations ### 3.1. Using "async/await" **Standard:** Use "async/await" syntax for handling asynchronous operations (e.g., API calls). **Why:** Provides a more readable and maintainable way to write asynchronous code compared to callbacks or promises. **Do This:** """typescript async function fetchData(): Promise<any> { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } catch (error: any) { console.error('Fetch error:', error); throw error; // Re-throw for handling in the calling function } } """ **Don't Do This:** """typescript // Anti-pattern: Using nested callbacks (callback hell) function fetchData(callback: (data: any) => void) { fetch('/api/data') .then(response => response.json()) .then(data => callback(data)) .catch(error => console.error(error)); } """ ### 3.2. Parallel API Calls **Standard:** Use "Promise.all" or "Promise.allSettled" to make multiple API calls in parallel when appropriate. **Why:** Improves performance by reducing the overall time required to fetch data from multiple sources. **Do This:** """typescript async function fetchMultipleData(): Promise<any[]> { try { const [data1, data2] = await Promise.all([ fetch('/api/data1').then(response => response.json()), fetch('/api/data2').then(response => response.json()), ]); return [data1, data2]; } catch (error: any) { console.error('Error fetching data:', error); return []; // Or handle error appropriately } } """ **Don't Do This:** """typescript // Anti-pattern: Making API calls sequentially (slower performance) async function fetchMultipleData(): Promise<any[]> { const data1 = await fetch('/api/data1').then(response => response.json()); const data2 = await fetch('/api/data2').then(response => response.json()); return [data1, data2]; } """ ## 4. Data Validation ### 4.1. Input Validation **Standard:** Validate user inputs and API request parameters to prevent injection attacks and ensure data integrity. Use libraries like "zod" or "yup" for schema validation. **Why:** Enhances security by preventing malicious data from being sent to the API and ensures that the application receives valid data. **Do This (using Zod):** First, install Zod: "npm install zod" """typescript import { z } from 'zod'; const UserSchema = z.object({ id: z.number().positive(), username: z.string().min(3).max(20), email: z.string().email(), }); type User = z.infer<typeof UserSchema>; async function createUser(userData: unknown): Promise<User> { try { const parsedData = UserSchema.parse(userData); // Send parsedData to the API console.log('Valid user data:', parsedData); return parsedData; } catch (error: any) { console.error('Validation error:', error.errors); throw new Error('Invalid user data'); } } // Usage const validUserData = { id: 1, username: 'johndoe', email: 'john.doe@example.com', }; const invalidUserData = { id: -1, username: 'jd', email: 'invalid-email', }; createUser(validUserData) .then(user => console.log('Created user:', user)) .catch(error => console.error('Failed to create user:', error.message)); createUser(invalidUserData) .catch(error => console.error('Failed to create user:', error.message)); """ **Don't Do This:** """typescript // Anti-pattern: Directly passing user input to the API without validation async function createUser(username: string, email: string): Promise<any> { const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify({ username, email }), //No validation! }); return response.json(); } """ ### 4.2. Response Validation **Standard:** Validate API responses to ensure data consistency and handle unexpected data formats. Use TypeScript interfaces/types and runtime validation (e.g., with "zod") to ensure data conforms to expected schemas. **Why:** Enhances robustness by preventing errors caused by malformed or unexpected API responses. **Do This**: """typescript import { z } from 'zod'; const ApiResponseSchema = z.object({ status: z.string(), data: z.array(z.object({ id: z.number(), name: z.string() })) }); type ApiResponse = z.infer<typeof ApiResponseSchema>; async function fetchData(url: string) { const response = await fetch(url); const data = await response.json(); try { const validatedData = ApiResponseSchema.parse(data); console.log("Validated data:", validatedData); return validatedData; } catch (error) { console.error("Response validation failed:", error); throw error; } } """ **Don't Do This:** """typescript // Anti-pattern: Assuming API responses are always in the expected format async function fetchData(): Promise<any> { const response = await fetch('/api/data'); const data = await response.json(); return data.items; // Assuming data.items always exists and is an array! } """ ## 5. Testing ### 5.1. Unit Testing **Standard:** Write unit tests for API client and service classes to ensure they function correctly. Mock API dependencies using libraries like "jest" or "sinon". **Why:** Improves code quality by verifying functionality and preventing regressions during refactoring. **Do This:** """typescript // api-client.test.ts import { DefaultAPIClient } from './api-client'; describe('DefaultAPIClient', () => { it('should fetch data successfully', async () => { const mockFetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: 'test data' }), }); global.fetch = mockFetch; // Mock the global fetch const apiClient = new DefaultAPIClient(); const data = await apiClient.fetchData('/api/data'); expect(mockFetch).toHaveBeenCalledWith('/api/data'); expect(data).toEqual({ data: 'test data' }); }); it('should handle errors when fetching data', async () => { const mockFetch = jest.fn().mockResolvedValue({ ok: false, status: 500, }); global.fetch = mockFetch; const apiClient = new DefaultAPIClient(); await expect(apiClient.fetchData('/api/data')).rejects.toThrow('HTTP error! status: 500'); }); }); """ **Don't Do This:** """typescript // Anti-pattern: No unit tests for API interaction logic """ ### 5.2. Integration Testing **Standard:** Write integration tests to verify the interaction between different components and the API. **Why:** Ensures that components work together correctly and the API integration functions as expected in a real-world scenario. These would likely utilize live dockerized APIs or mock servers running locally. **Do This:** The specific style would vary heavily depending on project setup so a simple example is not feasible in this document. **Don't Do This:** """typescript // Anti-pattern: Skipping integration tests and relying solely on unit tests """ By adhering to these standards, TypeScript developers can create robust, efficient, and maintainable API integrations. This document serves as a guideline for best practices, promoting consistency and quality across projects.
# State Management Standards for TypeScript This document outlines the standards for managing application state in TypeScript projects. It emphasizes modern approaches, best practices, and patterns to ensure maintainable, performant, and scalable applications. ## 1. General Principles ### 1.1. Immutability * **Do This:** Prefer immutable data structures whenever possible. * **Don't Do This:** Mutate state directly. **Why:** Immutability simplifies reasoning about state changes, prevents unexpected side effects, and enhances debugging. It also enables techniques like time-travel debugging and optimized rendering in UI frameworks. **Code Example:** """typescript // Immutable update using the spread operator interface User { id: number; name: string; age: number; } let user: User = { id: 1, name: "Alice", age: 30 }; // Good: Create a new object with the updated age let updatedUser: User = { ...user, age: 31 }; // Bad: Mutating the original object directly // user.age = 31; // Avoid this """ ### 1.2. Predictable State Transitions * **Do This:** Use explicit actions or events to trigger state changes. * **Don't Do This:** Rely on implicit or hidden state mutations. **Why:** Predictable state transitions make it easier to understand how the application's state evolves over time, leading to increased maintainability and debuggability. **Code Example (using a Redux-like pattern):** """typescript // Define action types type Action = | { type: "INCREMENT" } | { type: "DECREMENT" }; // Define the reducer function type CounterState = { count: number }; const initialState: CounterState = { count: 0 }; function counterReducer(state: CounterState = initialState, action: Action): CounterState { switch (action.type) { case "INCREMENT": return { ...state, count: state.count + 1 }; case "DECREMENT": return { ...state, count: state.count - 1 }; default: return state; } } // Example usage (simplified) let currentState = initialState; currentState = counterReducer(currentState, { type: "INCREMENT" }); console.log(currentState); // Output: { count: 1 } """ ### 1.3. Single Source of Truth * **Do This:** Centralize application state into a single store or context. * **Don't Do This:** Scatter state across multiple components or services without a clear hierarchy. **Why:** A single source of truth ensures consistency and avoids data duplication. It makes it easier to manage complex state interactions and provides a clear picture of the application's overall state. ### 1.4. Separation of Concerns * **Do This:** Separate state management logic from UI components and business logic. * **Don't Do This:** Mix state updates directly within UI event handlers or business logic functions. **Why:** Separation of concerns enhances testability, reusability, and maintainability. It allows you to modify state management mechanisms without affecting the rest of the application. ## 2. State Management Libraries and Patterns ### 2.1. Context API with "useReducer" (for simple to moderately complex state) * **Do This:** Use the "useReducer" hook in conjunction with the Context API for managing localized, moderately complex state in React components. * **Don't Do This:** Overuse the Context API for global state management in large applications, as it can lead to performance issues due to unnecessary re-renders. **Why:** Well-suited for managing state that is localized to a specific part of the application. "useReducer" provides a structured way to update state, similar to Redux, but without the overhead of a global store. The Context API provides a means to distribute and consume state across React components. **Code Example:** """typescript import React, { createContext, useContext, useReducer } from 'react'; // Define the state type and action types interface AuthState { isAuthenticated: boolean; user: { id: number; username: string } | null; } type AuthAction = | { type: 'LOGIN'; payload: { id: number; username: string } } | { type: 'LOGOUT' }; // Define the reducer function const authReducer = (state: AuthState, action: AuthAction): AuthState => { switch (action.type) { case 'LOGIN': return { ...state, isAuthenticated: true, user: action.payload }; case 'LOGOUT': return { ...state, isAuthenticated: false, user: null }; default: return state; } }; // Create the context interface AuthContextType { state: AuthState; dispatch: React.Dispatch<AuthAction>; } const AuthContext = createContext<AuthContextType | undefined>(undefined); // Create the provider component interface AuthProviderProps { children: React.ReactNode; } const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { const [state, dispatch] = useReducer(authReducer, { isAuthenticated: false, user: null }); return ( <AuthContext.Provider value={{ state, dispatch }}> {children} </AuthContext.Provider> ); }; // Create a custom hook to consume the context const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; export { AuthProvider, useAuth }; // Usage in a component const LoginComponent: React.FC = () => { const { dispatch } = useAuth(); const handleLogin = () => { // Simulate login dispatch({ type: 'LOGIN', payload: { id: 1, username: 'testuser' } }); }; return ( <button onClick={handleLogin}>Login</button> ); }; """ ### 2.2. Redux (for complex, application-wide state management) * **Do This:** Use Redux for managing global application state, especially in large, complex applications. Combine with Redux Toolkit to simplify configuration and reduce boilerplate. * **Don't Do This:** Use Redux for simple, component-local state. Opt for "useState" or "useReducer" in those cases. Avoid overly complex or deeply nested state structures that can hurt performance. **Why:** Redux provides a centralized store, predictable state transitions, and a rich ecosystem of middleware and tools. Redux Toolkit provides conventions and utilities which make Redux easier to use. **Code Example (using Redux Toolkit):** """typescript import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; // Define the state type interface CounterState { value: number; } // Define the initial state const initialState: CounterState = { value: 0, }; // Create a slice const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload; }, }, }); // Export actions and reducer export const { increment, decrement, incrementByAmount } = counterSlice.actions; export const counterReducer = counterSlice.reducer; // Configure the store const store = configureStore({ reducer: { counter: counterReducer, }, }); export default store; // Define RootState type export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; // Custom hooks for useSelector and useDispatch with proper typing export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; // Usage in a component import { useAppDispatch, useAppSelector } from './store'; const Counter: React.FC = () => { const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch(increment())}>Increment</button> <button onClick={() => dispatch(decrement())}>Decrement</button> <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button> </div> ); }; export default Counter; """ ### 2.3. Zustand (for manageable complexity and performance) * **Do This:** Consider Zustand as a simple, unopinionated state management solution. Suitable for applications where Redux might be overkill but "useReducer" is insufficient. Leverage selectors to optimize component re-renders. * **Don't Do This:** Use Zustand for highly complex global state scenarios requiring advanced features (e.g., time travel debugging, middleware). Over-rely on global state when component-local state is more appropriate. **Why:** Zustand is known for its simplicity and ease of use. It combines the benefits of mutable state with controlled updates, leading to good performance. It's an excellent middle-ground option. Selectors ensure components can subscribe to specific parts of the store, reducing unnecessary re-renders. **Code Example:** """typescript import { create } from 'zustand'; interface BearState { bears: number; increasePopulation: () => void; removeAllBears: () => void; } const useBearStore = create<BearState>((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })); // Example consumer import useBearStore from './store'; const BearCounter = () => { const bears = useBearStore((state) => state.bears); // Select only what is needed return <h1>{bears} around here ...</h1>; }; const Controls = () => { const increasePopulation = useBearStore((state) => state.increasePopulation); const removeAllBears = useBearStore((state) => state.removeAllBears); return ( <> <button onClick={increasePopulation}>one up</button> <button onClick={removeAllBears}>remove all</button> </> ) } """ ### 2.4. Jotai (for atomic state management) * **Do This:** Explore Jotai for its approach to state management based on atomic, derived state. It is valuable when needing fine-grained control over state dependencies. * **Don't Do This:** Attempt to port large Redux codebases to Jotai without careful consideration. Avoid creating excessively granular atoms that lead to performance bottlenecks. **Why:** Jotai is particularly well-suited for scenarios where components need to subscribe to very specific parts of the state, minimizing re-renders and improving performance. It promotes composition and code reuse. **Code Example:** """typescript import { atom, useAtom } from 'jotai'; // Create an atom const countAtom = atom(0); // Example consumer const CounterComponent = () => { const [count, setCount] = useAtom(countAtom); return ( <div> Count: {count} <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default CounterComponent; """ ### 2.5. XState (for managing complex state machines) * **Do This:** Use XState for orchestrating complex, stateful logic, like multi-step forms, workflows, or UI interactions with clearly defined states and transitions. Leverage TypeScript's type system to fully define state structures and events. * **Don't Do This:** Overuse XState for simple UI element state. Avoid creating state machines that are excessively complex or difficult to understand. **Why:** XState helps to manage complexity by providing a visual and declarative way to define states and transitions. TypeScript integration further enhances the development experience and ensures type safety. **Code Example:** """typescript import { createMachine, assign } from 'xstate'; import { useMachine } from '@xstate/react'; // Define the state machine context interface ContextType { count: number; } // Define the state machine events type EventType = | { type: 'INCREMENT' } | { type: 'DECREMENT' }; // Create the state machine const counterMachine = createMachine<ContextType, EventType>( { id: 'counter', initial: 'idle', context: { count: 0, }, states: { idle: { on: { INCREMENT: { actions: assign({ count: (context) => context.count + 1 }), }, DECREMENT: { actions: assign({ count: (context) => context.count - 1 }), }, }, }, }, } ); // React component using the state machine const CounterComponent = () => { const [state, send] = useMachine(counterMachine); return ( <div> Count: {state.context.count} <button onClick={() => send('INCREMENT')}>Increment</button> <button onClick={() => send('DECREMENT')}>Decrement</button> </div> ); }; export default CounterComponent; """ ## 3. Reactive Programming with RxJS ### 3.1. Observables for Asynchronous Data Streams * **Do This:** Use RxJS Observables to handle asynchronous data streams, user input, and event-driven interactions. * **Don't Do This:** Use RxJS for simple, synchronous operations that can be handled with standard JavaScript methods. Overuse or abuse Subjects, leading to uncontrolled side effects. **Why:** Observables provide a powerful and flexible way to manage asynchronous data over time. RxJS offers a wide range of operators for transforming, filtering, and combining streams of data. **Code Example:** """typescript import { fromEvent, interval } from 'rxjs'; import { map, filter, take, scan } from 'rxjs/operators'; // Create an observable from a DOM event const button = document.getElementById('myButton'); if (button) { const click$ = fromEvent(button, 'click'); // Ensure button isn't null click$.pipe( map(() => 1), scan((acc, val) => acc + val, 0) ).subscribe(count => { console.log("Button clicked ${count} times"); }); } // Create an observable from an interval const interval$ = interval(1000); interval$.pipe( filter(value => value % 2 === 0), map(value => "Tick: ${value}"), take(5) ).subscribe(message => { console.log(message); }); """ ### 3.2. Subjects for Multicasting * **Do This:** Use RxJS Subjects to multicast values to multiple observers. Use "BehaviorSubject" or "ReplaySubject" when you need to provide an initial value or replay past values, respectively. * **Don't Do This:** Expose Subjects directly to components. Encapsulate them within services or state management solutions to control the flow of data. **Why:** Subjects act as both an Observable and an Observer, allowing you to push values to multiple subscribers. They are useful for creating shared data streams or event buses. **Code Example:** """typescript import { Subject } from 'rxjs'; // Create a Subject const mySubject = new Subject<string>(); // Subscribe to the Subject mySubject.subscribe(value => { console.log("Observer 1: ${value}"); }); mySubject.subscribe(value => { console.log("Observer 2: ${value}"); }); // Push values to the Subject mySubject.next('Hello'); mySubject.next('World'); """ ## 4. Practical Implementation Considerations ### 4.1. TypeScript Typing * **Do This:** Use strong typing to define the shape of your state, actions, and reducers. * **Don't Do This:** Use "any" or "unknown" types excessively, which defeats the purpose of using TypeScript. **Why:** Strong typing improves code maintainability, prevents runtime errors, and enhances code completion in IDEs. **Code Example:** """typescript interface Product { id: number; name: string; price: number; } type ProductAction = | { type: "ADD_PRODUCT"; payload: Product } | { type: "REMOVE_PRODUCT"; payload: number }; interface ProductState { products: Product[]; } """ ### 4.2. Performance Optimization * **Do This:** Use memoization techniques (e.g., "useMemo", "useCallback", "React.memo") to prevent unnecessary re-renders. Select only the necessary data from the state to avoid triggering updates when unrelated data changes. * **Don't Do This:** Neglect performance optimization, especially in large applications with frequent state updates. Deeply nested state often triggers expensive rerenders. **Why:** Optimizing performance ensures a smooth user experience and reduces resource consumption. Memoization and selective updates can significantly improve rendering performance. ### 4.3. Testing * **Do This:** Write unit tests for your reducers, selectors, and effects to ensure they behave as expected. * **Don't Do This:** Neglect testing state management logic, which can lead to unexpected behavior and difficult-to-debug issues. **Why:** Thorough testing is essential for maintaining the integrity of your state management system. Unit tests provide confidence that your code is working correctly and prevent regressions when making changes. ### 4.4. Error Handling * **Do This:** Properly handle errors that may occur during state updates, asynchronous operations, or API calls. Display user-friendly error messages and provide mechanisms for recovering from errors. * **Don't Do This:** Ignore errors or allow them to propagate silently, which can lead to a poor user experience and data corruption. **Why:** Robust error handling ensures that your application can gracefully recover from unexpected errors and prevents data loss or corruption.