Tech Stack: Node.js 20+, React 18+, Next.js 14+, Express.js 4+, MongoDB 7+, TypeScript 5+
This document defines mandatory standards and best practices for building scalable, maintainable, secure, and highβperformance applications using the MERN stack (MongoDB, Express.js, React, Node.js) with Next.js and TypeScript. All developers must follow these guidelines consistently.
- SOLID principles must be followed
- YAGNI (You Aren't Gonna Need It) - Don't add functionality until it's necessary
- Separation of Concerns (SoC)
- Single Responsibility for components, functions, and modules
- Fail fast, validate early
- Explicit over implicit
- Readable code > clever code
- KISS (Keep It Simple, Stupid) - Prefer simple solutions over complex ones
- DRY (Don't Repeat Yourself) - Avoid code duplication
- Composition over Inheritance - Especially in React
- Code must be:
- Readable
- Testable
- Extensible
- Documented
- Type-safe (TypeScript)
- No business logic in components or API routes
- No direct database queries outside repositories/services
- Consistent code style across the codebase
- Minimum TypeScript 5.0+
- Use strict mode in
tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}- Always use:
- Explicit type annotations for function parameters and return types
- Interfaces for object shapes
- Type aliases for unions and complex types
- Enums for fixed sets of values
- Generics for reusable code
- Utility types (
Partial,Pick,Omit,Record, etc.) - Discriminated unions for type narrowing
// β
Good - Explicit types
interface User {
id: string;
email: string;
name: string;
role: UserRole;
createdAt: Date;
}
enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
}
type UserCreateInput = Omit<User, 'id' | 'createdAt'>;
type UserUpdateInput = Partial<Pick<User, 'name' | 'email'>>;
function createUser(input: UserCreateInput): Promise<User> {
// Implementation
}
// β Bad - Implicit any
function createUser(input) {
// Implementation
}- Use TypeScript 5.0+ features:
- const assertions for immutable data
- satisfies operator for type checking without widening
- Template literal types for string manipulation
- Mapped types for transformations
- Conditional types for advanced type manipulation
- Type predicates for type guards
// const assertions
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// satisfies operator
const theme = {
primary: '#007bff',
secondary: '#6c757d',
} satisfies Record<string, string>;
// Type predicates
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}- Use strict ESLint rules for TypeScript
- No
anytypes without justification - No
@ts-ignorewithout explanation - Prefer
unknownoveranyfor type-safe handling - Use type-only imports when possible
// β
Good
import type { User } from './types';
import { createUser } from './services';
// β Bad
import { User, createUser } from './types';- Use ESLint with strict rules
- Use Prettier for code formatting
- Follow Airbnb or Standard style guide
- Use ES modules (
import/export) over CommonJS - Prefer const over
let, avoidvar - Use arrow functions for callbacks
- Use template literals for string concatenation
- Use destructuring for object/array access
- Use optional chaining (
?.) and nullish coalescing (??)
// β
Good
const user = await getUserById(id);
const { name, email } = user ?? {};
const displayName = name ?? 'Anonymous';
const emailDomain = email?.split('@')[1];
// β Bad
const user = await getUserById(id);
const name = user && user.name ? user.name : 'Anonymous';- Always use
async/awaitover Promises chains - Handle errors with try/catch
- Use
Promise.all()for parallel operations - Use
Promise.allSettled()when some failures are acceptable - Avoid mixing async/await with
.then()/.catch()
// β
Good
async function fetchUserData(userId: string): Promise<UserData> {
try {
const [user, posts, comments] = await Promise.all([
getUserById(userId),
getPostsByUserId(userId),
getCommentsByUserId(userId),
]);
return { user, posts, comments };
} catch (error) {
logger.error('Failed to fetch user data', { userId, error });
throw new UserDataFetchError(userId, error);
}
}
// β Bad
function fetchUserData(userId: string) {
return getUserById(userId)
.then(user => {
return getPostsByUserId(userId).then(posts => {
return getCommentsByUserId(userId).then(comments => {
return { user, posts, comments };
});
});
})
.catch(error => {
console.log(error);
});
}- Use custom error classes
- Always handle errors explicitly
- Provide meaningful error messages
- Log errors with context
- Use error boundaries in React
// Custom error classes
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
}
}
// Error handling
async function updateUser(id: string, data: UserUpdateInput): Promise<User> {
try {
const user = await userRepository.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return await userRepository.update(id, data);
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('Failed to update user', { id, data, error });
throw new DatabaseError('Failed to update user', error);
}
}- Use functional components exclusively (no class components)
- Use TypeScript for all components
- Keep components small and focused (single responsibility)
- Extract reusable logic into custom hooks
- Use composition over prop drilling
- Prefer explicit props over spreading
// β
Good - Explicit props, small component
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export function Button({
label,
onClick,
variant = 'primary',
disabled = false,
}: ButtonProps): JSX.Element {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// β Bad - Large component, implicit props
export function Button(props: any) {
// Too much logic, unclear props
}- Use hooks at the top level only (not in loops/conditions)
- Extract complex logic into custom hooks
- Use
useMemofor expensive computations - Use
useCallbackfor function references passed to children - Use
useEffectwith proper dependencies - Clean up side effects in
useEffect
// β
Good - Custom hook
function useUserPosts(userId: string | null) {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!userId) return;
let cancelled = false;
setLoading(true);
setError(null);
fetchUserPosts(userId)
.then(data => {
if (!cancelled) {
setPosts(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
return { posts, loading, error };
}
// β Bad - Logic in component
function UserPosts({ userId }: { userId: string }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetchUserPosts(userId).then(setPosts);
// Missing cleanup, missing loading/error states
}, []);
}- Use local state (
useState) for component-specific state - Use Context API for shared state that doesn't change frequently
- Use state management libraries (Zustand, Redux Toolkit, Jotai) for complex global state
- Avoid prop drilling - use Context or state management
- Keep state as close to where it's used as possible
// β
Good - Zustand store
import { create } from 'zustand';
interface AuthStore {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: () => boolean;
}
export const useAuthStore = create<AuthStore>((set, get) => ({
user: null,
token: null,
login: async (email, password) => {
const response = await authService.login(email, password);
set({ user: response.user, token: response.token });
},
logout: () => {
set({ user: null, token: null });
},
isAuthenticated: () => {
return get().user !== null && get().token !== null;
},
}));- Use
React.memofor expensive components - Use
useMemofor expensive calculations - Use
useCallbackfor stable function references - Code split with
React.lazyandSuspense - Virtualize long lists with
react-windoworreact-virtual - Avoid creating objects/functions in render
// β
Good - Memoized component
const ExpensiveList = React.memo<{ items: Item[] }>(({ items }) => {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return (
<ul>
{sortedItems.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
});
// β Bad - Re-renders on every parent update
function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items
.sort((a, b) => a.name.localeCompare(b.name))
.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
}- One component per file
- Co-locate related files (component, styles, tests, types)
- Use index files for clean imports
- Group by feature, not by type
components/
UserProfile/
UserProfile.tsx
UserProfile.test.tsx
UserProfile.module.css
types.ts
index.ts
Button/
Button.tsx
Button.test.tsx
Button.module.css
index.ts
- Implement Error Boundaries for error handling
- Catch errors in component tree
- Provide fallback UI
- Log errors to error tracking service
- Use error boundaries at appropriate levels
// β
Good - Error Boundary
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log to error tracking service
console.error('Error caught by boundary:', error, errorInfo);
// Sentry.captureException(error, { contexts: { react: errorInfo } });
}
render(): ReactNode {
if (this.state.hasError) {
return (
this.props.fallback || (
<div>
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
);
}
return this.props.children;
}
}- Use React Hook Form or Formik for form management
- Validate on both client and server
- Provide clear error messages
- Handle form submission states
- Implement proper accessibility
// β
Good - React Hook Form with Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name must be at least 2 characters'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type UserFormData = z.infer<typeof userSchema>;
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
});
const onSubmit = async (data: UserFormData) => {
try {
await createUser(data);
} catch (error) {
// Handle error
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email')}
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<span role="alert">{errors.email.message}</span>
)}
</div>
{/* More fields... */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}- Use React Query (TanStack Query) or SWR for server state
- Implement proper loading and error states
- Use caching and refetching strategies
- Handle optimistic updates
- Implement pagination and infinite scrolling
// β
Good - React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: UserCreateInput) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}- One component per file
- Co-locate related files (component, styles, tests, types)
- Use index files for clean imports
- Group by feature, not by type
components/
UserProfile/
UserProfile.tsx
UserProfile.test.tsx
UserProfile.module.css
types.ts
index.ts
Button/
Button.tsx
Button.test.tsx
Button.module.css
index.ts
app/ # App Router (Next.js 13+)
(auth)/ # Route groups
login/
page.tsx
register/
page.tsx
(dashboard)/
dashboard/
page.tsx
loading.tsx
error.tsx
api/ # API routes
users/
route.ts
posts/
[id]/
route.ts
layout.tsx
page.tsx
globals.css
components/ # Shared components
ui/
forms/
layout/
lib/ # Utilities and helpers
utils/
constants/
validations/
hooks/ # Custom React hooks
useAuth.ts
useUser.ts
types/ # TypeScript types
user.ts
post.ts
public/ # Static assets
images/
icons/
styles/ # Global styles
globals.css
variables.css
- Use Server Components by default (React Server Components)
- Use Client Components (
'use client') only when needed - Use Server Actions for mutations
- Use Route Handlers for API endpoints
- Leverage Streaming and Suspense for better UX
- Use Parallel Routes and Intercepting Routes when appropriate
// β
Good - Server Component (default)
// app/posts/page.tsx
import { getPosts } from '@/lib/api/posts';
export default async function PostsPage() {
const posts = await getPosts(); // Server-side data fetching
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// β
Good - Client Component (when needed)
// app/components/PostForm.tsx
'use client';
import { useState } from 'react';
import { createPost } from '@/app/actions/posts';
export function PostForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
await createPost(formData);
} finally {
setIsSubmitting(false);
}
}
return (
<form action={handleSubmit}>
{/* Form fields */}
</form>
);
}- Use Server Components for data fetching when possible
- Use Server Actions for form submissions and mutations
- Use Route Handlers for external API proxies
- Implement proper error handling and loading states
- Use revalidation strategies (ISR, on-demand, time-based)
// β
Good - Server Action
// app/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { createPostSchema } from '@/lib/validations/post';
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
};
const validatedData = createPostSchema.parse(rawData);
try {
const post = await postService.create(validatedData);
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
} catch (error) {
throw new Error('Failed to create post');
}
}
// β
Good - Route Handler
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get('page') ?? '1';
const limit = searchParams.get('limit') ?? '10';
const posts = await postService.getPaginated({
page: Number(page),
limit: Number(limit),
});
return NextResponse.json({ posts });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}- Use metadata API for page metadata
- Generate dynamic metadata when needed
- Use Open Graph and Twitter Card metadata
- Implement proper sitemap and robots.txt
// β
Good - Metadata
// app/posts/[id]/page.tsx
import type { Metadata } from 'next';
type Props = {
params: { id: string };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPostById(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.imageUrl],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
},
};
}- Use middleware for authentication, redirects, and headers
- Keep middleware lightweight and fast
- Use matcher to limit middleware execution
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Redirect authenticated users away from auth pages
if (request.nextUrl.pathname.startsWith('/login')) {
if (token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/register'],
};src/
controllers/ # Request handlers
userController.ts
postController.ts
services/ # Business logic
userService.ts
postService.ts
repositories/ # Data access
userRepository.ts
postRepository.ts
models/ # Data models/schemas
User.ts
Post.ts
routes/ # Route definitions
userRoutes.ts
postRoutes.ts
middleware/ # Custom middleware
auth.ts
validation.ts
errorHandler.ts
utils/ # Utilities
logger.ts
errors.ts
types/ # TypeScript types
user.ts
post.ts
config/ # Configuration
database.ts
env.ts
app.ts # Express app setup
server.ts # Server entry point
- Use TypeScript for all Express code
- Use async/await for route handlers
- Use middleware for cross-cutting concerns
- Implement proper error handling middleware
- Use route handlers (thin controllers)
- Validate input with validation libraries (Zod, Joi, Yup)
- Use helmet for security headers
- Use cors for CORS configuration
- Use compression for response compression
- Use rate limiting for API protection
- Configure CORS properly for production
- Whitelist specific origins
- Handle credentials correctly
- Use environment variables for allowed origins
// β
Good - CORS configuration
import cors from 'cors';
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') ?? [];
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
app.use(cors(corsOptions));- Implement rate limiting on all API endpoints
- Use different limits for different endpoints
- Provide rate limit headers in response
- Handle rate limit errors gracefully
// β
Good - Rate limiting
import rateLimit from 'express-rate-limit';
// General API rate limiter
const apiLimiter = 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 later.',
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false, // Disable `X-RateLimit-*` headers
});
// Strict rate limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
skipSuccessfulRequests: true,
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);- Use TypeScript for all Express code
- Use async/await for route handlers
- Use middleware for cross-cutting concerns
- Implement proper error handling middleware
- Use route handlers (thin controllers)
- Validate input with validation libraries (Zod, Joi, Yup)
- Use helmet for security headers
- Use cors for CORS configuration
- Use compression for response compression
- Use rate limiting for API protection
// β
Good - Express app setup
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { errorHandler } from './middleware/errorHandler';
import { requestLogger } from './middleware/logger';
import userRoutes from './routes/userRoutes';
import postRoutes from './routes/postRoutes';
const app = express();
// Security middleware
app.use(helmet());
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
credentials: true,
})
);
// Performance middleware
app.use(compression());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Logging
app.use(requestLogger);
// Routes
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handling (must be last)
app.use(errorHandler);
export default app;- Keep controllers thin - delegate to services
- Handle errors and return appropriate status codes
- Validate input before processing
- Use proper HTTP status codes
- Return consistent response format
// β
Good - Thin controller
// src/controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { userService } from '../services/userService';
import { createUserSchema, updateUserSchema } from '../validations/user';
export const userController = {
async getUsers(req: Request, res: Response, next: NextFunction) {
try {
const { page = 1, limit = 10, search } = req.query;
const users = await userService.getUsers({
page: Number(page),
limit: Number(limit),
search: search as string,
});
res.json({
success: true,
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
},
});
} catch (error) {
next(error);
}
},
async getUserById(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await userService.getUserById(id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found',
});
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
},
async createUser(req: Request, res: Response, next: NextFunction) {
try {
const validatedData = createUserSchema.parse(req.body);
const user = await userService.createUser(validatedData);
res.status(201).json({ success: true, data: user });
} catch (error) {
next(error);
}
},
async updateUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const validatedData = updateUserSchema.parse(req.body);
const user = await userService.updateUser(id, validatedData);
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
},
async deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
await userService.deleteUser(id);
res.status(204).send();
} catch (error) {
next(error);
}
},
};- Contain all business logic in services
- Services depend on repositories, not models directly
- Use dependency injection for testability
- Handle business rules and validations
- Return domain objects, not database documents
// β
Good - Service layer
// src/services/userService.ts
import { userRepository } from '../repositories/userRepository';
import { User, UserCreateInput, UserUpdateInput } from '../types/user';
import { NotFoundError, ValidationError } from '../utils/errors';
import { hashPassword, comparePassword } from '../utils/password';
export const userService = {
async getUsers(options: {
page: number;
limit: number;
search?: string;
}): Promise<{ users: User[]; total: number }> {
return userRepository.findMany(options);
},
async getUserById(id: string): Promise<User | null> {
return userRepository.findById(id);
},
async createUser(input: UserCreateInput): Promise<User> {
// Business logic: Check if email exists
const existingUser = await userRepository.findByEmail(input.email);
if (existingUser) {
throw new ValidationError('Email already exists', 'email', input.email);
}
// Business logic: Hash password
const hashedPassword = await hashPassword(input.password);
return userRepository.create({
...input,
password: hashedPassword,
});
},
async updateUser(id: string, input: UserUpdateInput): Promise<User> {
const user = await userRepository.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
// Business logic: If email changed, check uniqueness
if (input.email && input.email !== user.email) {
const existingUser = await userRepository.findByEmail(input.email);
if (existingUser) {
throw new ValidationError('Email already exists', 'email', input.email);
}
}
return userRepository.update(id, input);
},
async deleteUser(id: string): Promise<void> {
const user = await userRepository.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
await userRepository.delete(id);
},
};- Abstract data access logic
- Use MongoDB with Mongoose or native driver
- Implement interfaces for testability
- Handle database-specific operations
- Return domain objects
// β
Good - Repository layer
// src/repositories/userRepository.ts
import { User, UserCreateInput, UserUpdateInput } from '../types/user';
import { UserModel } from '../models/User';
export const userRepository = {
async findMany(options: {
page: number;
limit: number;
search?: string;
}): Promise<{ users: User[]; total: number }> {
const query: Record<string, unknown> = {};
if (options.search) {
query.$or = [
{ name: { $regex: options.search, $options: 'i' } },
{ email: { $regex: options.search, $options: 'i' } },
];
}
const [users, total] = await Promise.all([
UserModel.find(query)
.skip((options.page - 1) * options.limit)
.limit(options.limit)
.lean()
.exec(),
UserModel.countDocuments(query),
]);
return {
users: users.map(user => this.toDomain(user)),
total,
};
},
async findById(id: string): Promise<User | null> {
const user = await UserModel.findById(id).lean().exec();
return user ? this.toDomain(user) : null;
},
async findByEmail(email: string): Promise<User | null> {
const user = await UserModel.findOne({ email }).lean().exec();
return user ? this.toDomain(user) : null;
},
async create(input: UserCreateInput): Promise<User> {
const user = await UserModel.create(input);
return this.toDomain(user.toObject());
},
async update(id: string, input: UserUpdateInput): Promise<User> {
const user = await UserModel.findByIdAndUpdate(
id,
{ $set: input },
{ new: true, lean: true }
).exec();
if (!user) {
throw new Error('User not found');
}
return this.toDomain(user);
},
async delete(id: string): Promise<void> {
await UserModel.findByIdAndDelete(id).exec();
},
// Transform database document to domain object
toDomain(doc: unknown): User {
const user = doc as User;
return {
id: user._id.toString(),
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt,
};
},
};- Use custom error classes
- Implement global error handler middleware
- Return consistent error responses
- Log errors with context
- Don't expose internal errors to clients
// β
Good - Error handling
// src/utils/errors.ts
export class AppError extends Error {
constructor(
message: string,
public readonly statusCode: number = 500,
public readonly isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, 404);
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message, 400);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
}
}
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../utils/errors';
import { logger } from '../utils/logger';
export function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction
): void {
// Log error
logger.error('Error occurred', {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});
// Handle known errors
if (error instanceof AppError) {
res.status(error.statusCode).json({
success: false,
error: error.message,
...(process.env.NODE_ENV === 'development' && {
stack: error.stack,
}),
});
return;
}
// Handle unknown errors
res.status(500).json({
success: false,
error: 'Internal server error',
...(process.env.NODE_ENV === 'development' && {
message: error.message,
stack: error.stack,
}),
});
}- Use Mongoose for schema definition and validation
- Define schemas with proper types and validation
- Use indexes for frequently queried fields
- Use virtuals for computed properties
- Use methods and statics for schema-level logic
- Use pre/post hooks for lifecycle events
// β
Good - Mongoose schema
// src/models/User.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IUser extends Document {
email: string;
name: string;
password: string;
role: 'admin' | 'user' | 'moderator';
createdAt: Date;
updatedAt: Date;
comparePassword(candidatePassword: string): Promise<boolean>;
}
const userSchema = new Schema<IUser>(
{
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email'],
index: true,
},
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters'],
maxlength: [50, 'Name cannot exceed 50 characters'],
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [8, 'Password must be at least 8 characters'],
select: false, // Don't include in queries by default
},
role: {
type: String,
enum: ['admin', 'user', 'moderator'],
default: 'user',
index: true,
},
},
{
timestamps: true,
toJSON: {
transform: (doc, ret) => {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
delete ret.password;
return ret;
},
},
}
);
// Indexes
userSchema.index({ email: 1 });
userSchema.index({ role: 1 });
userSchema.index({ createdAt: -1 });
// Methods
userSchema.methods.comparePassword = async function (
candidatePassword: string
): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password);
};
// Pre-save hook
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
export const UserModel = mongoose.model<IUser>('User', userSchema);- Use projection to limit returned fields
- Use lean() for read-only queries (faster)
- Use indexes for frequently queried fields
- Use aggregation pipelines for complex queries
- Avoid N+1 queries - use
populate()or aggregation - Use pagination for large datasets
- Use cursor-based pagination for real-time data
// β
Good - Optimized queries
// Get users with pagination and projection
const users = await UserModel.find({ role: 'user' })
.select('name email createdAt') // Projection
.lean() // Faster, returns plain objects
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(limit)
.exec();
// Use aggregation for complex queries
const userStats = await UserModel.aggregate([
{ $match: { role: 'user' } },
{
$group: {
_id: '$role',
count: { $sum: 1 },
avgAge: { $avg: '$age' },
},
},
]);
// Populate relationships efficiently
const posts = await PostModel.find({ authorId })
.populate('author', 'name email') // Only select needed fields
.lean()
.exec();- Use transactions for multi-document operations
- Keep transactions short
- Handle transaction errors properly
- Use sessions for transaction management
// β
Good - Transaction usage
import mongoose from 'mongoose';
async function transferFunds(
fromUserId: string,
toUserId: string,
amount: number
): Promise<void> {
const session = await mongoose.startSession();
session.startTransaction();
try {
// Update sender balance
await UserModel.findByIdAndUpdate(
fromUserId,
{ $inc: { balance: -amount } },
{ session }
);
// Update receiver balance
await UserModel.findByIdAndUpdate(
toUserId,
{ $inc: { balance: amount } },
{ session }
);
// Create transaction record
await TransactionModel.create(
[
{
fromUserId,
toUserId,
amount,
type: 'transfer',
},
],
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}- Use Jest for unit and integration tests
- Use React Testing Library for React component tests
- Use Supertest for API endpoint tests
- Aim for 80%+ code coverage
- Write tests before or alongside code (TDD/BDD)
- Test individual functions and methods
- Mock external dependencies
- Test edge cases and error scenarios
- Keep tests isolated and independent
// β
Good - Unit test
// src/services/__tests__/userService.test.ts
import { userService } from '../userService';
import { userRepository } from '../../repositories/userRepository';
import { NotFoundError, ValidationError } from '../../utils/errors';
jest.mock('../../repositories/userRepository');
describe('userService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('getUserById', () => {
it('should return user when found', async () => {
const mockUser = { id: '1', email: '[email protected]', name: 'Test' };
(userRepository.findById as jest.Mock).mockResolvedValue(mockUser);
const result = await userService.getUserById('1');
expect(result).toEqual(mockUser);
expect(userRepository.findById).toHaveBeenCalledWith('1');
});
it('should return null when user not found', async () => {
(userRepository.findById as jest.Mock).mockResolvedValue(null);
const result = await userService.getUserById('1');
expect(result).toBeNull();
});
});
describe('createUser', () => {
it('should create user with hashed password', async () => {
const input = {
email: '[email protected]',
name: 'Test',
password: 'password123',
};
const mockUser = { ...input, id: '1', password: 'hashed' };
(userRepository.findByEmail as jest.Mock).mockResolvedValue(null);
(userRepository.create as jest.Mock).mockResolvedValue(mockUser);
const result = await userService.createUser(input);
expect(result).toEqual(mockUser);
expect(userRepository.findByEmail).toHaveBeenCalledWith(input.email);
});
it('should throw ValidationError when email exists', async () => {
const input = {
email: '[email protected]',
name: 'Test',
password: 'password123',
};
(userRepository.findByEmail as jest.Mock).mockResolvedValue({
id: '1',
email: input.email,
});
await expect(userService.createUser(input)).rejects.toThrow(
ValidationError
);
});
});
});- Test API endpoints end-to-end
- Use test database
- Clean up test data after tests
- Test authentication and authorization
// β
Good - Integration test
// src/routes/__tests__/userRoutes.test.ts
import request from 'supertest';
import app from '../../app';
import { UserModel } from '../../models/User';
import { connectDB, disconnectDB } from '../../config/database';
beforeAll(async () => {
await connectDB(process.env.TEST_MONGODB_URI!);
});
afterAll(async () => {
await UserModel.deleteMany({});
await disconnectDB();
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: '[email protected]',
name: 'Test User',
password: 'password123',
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.email).toBe(userData.email);
expect(response.body.data.name).toBe(userData.name);
expect(response.body.data.password).toBeUndefined();
});
it('should return 400 for invalid email', async () => {
const userData = {
email: 'invalid-email',
name: 'Test User',
password: 'password123',
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(response.body.success).toBe(false);
});
});- Test user interactions, not implementation
- Use React Testing Library queries
- Test accessibility
- Mock external dependencies
// β
Good - React component test
// components/UserProfile/__tests__/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from '../UserProfile';
import { useAuthStore } from '@/stores/authStore';
jest.mock('@/stores/authStore');
describe('UserProfile', () => {
const mockUser = {
id: '1',
email: '[email protected]',
name: 'Test User',
};
beforeEach(() => {
(useAuthStore as jest.Mock).mockReturnValue({
user: mockUser,
updateProfile: jest.fn(),
});
});
it('should display user information', () => {
render(<UserProfile />);
expect(screen.getByText('Test User')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
});
it('should allow editing user name', async () => {
const user = userEvent.setup();
const updateProfile = jest.fn();
(useAuthStore as jest.Mock).mockReturnValue({
user: mockUser,
updateProfile,
});
render(<UserProfile />);
const editButton = screen.getByRole('button', { name: /edit/i });
await user.click(editButton);
const nameInput = screen.getByLabelText(/name/i);
await user.clear(nameInput);
await user.type(nameInput, 'Updated Name');
const saveButton = screen.getByRole('button', { name: /save/i });
await user.click(saveButton);
await waitFor(() => {
expect(updateProfile).toHaveBeenCalledWith({
name: 'Updated Name',
});
});
});
});- Use JWT tokens for authentication
- Store tokens securely (httpOnly cookies preferred)
- Implement refresh tokens for better security
- Use bcrypt for password hashing (cost factor 12+)
- Implement rate limiting on auth endpoints
- Use HTTPS in production
- Validate and sanitize all inputs
// β
Good - JWT authentication
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { UnauthorizedError } from '../utils/errors';
interface AuthRequest extends Request {
user?: {
id: string;
email: string;
role: string;
};
}
export function authenticate(
req: AuthRequest,
res: Response,
next: NextFunction
): void {
try {
const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
if (!token) {
throw new UnauthorizedError('Authentication required');
}
const decoded = jwt.verify(
token,
process.env.JWT_SECRET!
) as { id: string; email: string; role: string };
req.user = decoded;
next();
} catch (error) {
next(new UnauthorizedError('Invalid or expired token'));
}
}
export function authorize(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
return next(new UnauthorizedError('Authentication required'));
}
if (!roles.includes(req.user.role)) {
return next(
new UnauthorizedError('Insufficient permissions')
);
}
next();
};
}- Validate all inputs on the server side
- Use Zod or Joi for schema validation
- Sanitize user inputs
- Use parameterized queries (MongoDB handles this)
- Implement CSRF protection for state-changing operations
// β
Good - Input validation with Zod
// src/validations/user.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email('Invalid email address'),
name: z
.string()
.min(2, 'Name must be at least 2 characters')
.max(50, 'Name cannot exceed 50 characters'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase, and number'
),
});
export const updateUserSchema = createUserSchema.partial();
// Middleware
export function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
success: false,
error: 'Validation failed',
details: error.errors,
});
return;
}
next(error);
}
};
}- Use helmet for security headers
- Implement CORS properly
- Use Content Security Policy (CSP)
- Prevent XSS attacks
- Prevent clickjacking
// β
Good - Security headers
import helmet from 'helmet';
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
crossOriginEmbedderPolicy: false,
})
);- Use code splitting and lazy loading
- Optimize images (Next.js Image component)
- Use memoization (
React.memo,useMemo,useCallback) - Implement virtual scrolling for long lists
- Use CDN for static assets
- Minimize bundle size (analyze with webpack-bundle-analyzer)
- Use Service Workers for caching
// β
Good - Code splitting
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// β
Good - Image optimization (Next.js)
import Image from 'next/image';
function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={500}
height={500}
loading="lazy"
placeholder="blur"
/>
);
}- Use caching (Redis) for frequently accessed data
- Implement database indexing
- Use connection pooling
- Implement pagination for large datasets
- Use compression (gzip/brotli)
- Optimize database queries
- Use background jobs for heavy operations
// β
Good - Caching with Redis
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
async function getUserById(id: string): Promise<User | null> {
// Check cache first
const cached = await redis.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await userRepository.findById(id);
if (user) {
// Cache for 1 hour
await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
}
return user;
}- Create indexes on frequently queried fields
- Use compound indexes for multi-field queries
- Avoid N+1 queries
- Use projection to limit returned fields
- Use lean() for read-only queries
- Implement pagination
- Monitor slow queries
// β
Good - Database indexes
userSchema.index({ email: 1 }); // Single field
userSchema.index({ role: 1, createdAt: -1 }); // Compound index
userSchema.index({ 'location.coordinates': '2dsphere' }); // Geospatial- Use custom error classes
- Implement global error handlers
- Return consistent error responses
- Log errors with context
- Don't expose sensitive information
- Use structured logging (Winston, Pino)
- Log at appropriate levels (error, warn, info, debug)
- Include context in logs (user ID, request ID, etc.)
- Don't log sensitive data (passwords, tokens)
- Use log aggregation in production
// β
Good - Structured logging
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}
// Usage
logger.info('User created', { userId: user.id, email: user.email });
logger.error('Failed to create user', { error: error.message, stack: error.stack });- Use npm, yarn, or pnpm consistently across the project
- Prefer pnpm or yarn for better dependency resolution
- Lock dependency versions with
package-lock.json,yarn.lock, orpnpm-lock.yaml - Always commit lock files to version control
- Use exact versions (
1.2.3) or caret ranges (^1.2.3) appropriately - Avoid wildcard versions (
*)
// β
Good - package.json
{
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.5.0",
"typescript": "^5.1.6"
}
}
// β Bad
{
"dependencies": {
"express": "*",
"mongoose": "latest"
}
}- Regularly update dependencies (
npm audit,npm outdated) - Review security advisories (
npm audit) - Use
npm ciin CI/CD pipelines (faster, more reliable) - Pin critical dependencies to exact versions
- Document why specific versions are required
- Use peer dependencies correctly
# Check for vulnerabilities
npm audit
# Fix vulnerabilities automatically
npm audit fix
# Check outdated packages
npm outdated
# Update dependencies
npm update
# Install exact versions (CI/CD)
npm ci- Prefer well-maintained packages (check GitHub stars, recent commits)
- Check package statistics (downloads, maintenance status)
- Review package code quality and security
- Prefer official packages over third-party alternatives
- Check bundle size impact for frontend packages
- Verify TypeScript support
- Use npm workspaces, yarn workspaces, or pnpm workspaces for monorepos
- Share common dependencies at root level
- Use workspace protocol for internal packages
- Keep workspace dependencies in sync
// β
Good - Workspace setup
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
],
"private": true
}- Use
.envfiles for local development - Use
.env.exampleas a template (commit this) - Never commit
.envfiles to version control - Use different
.envfiles for different environments - Document all required environment variables
- Use dotenv or dotenv-expand for loading
# β
Good - .env.example
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
JWT_EXPIRES_IN=7d
NODE_ENV=development
PORT=3000
REDIS_URL=redis://localhost:6379- Store configuration in
config/directory - Use
process.envwith defaults - Validate environment variables on startup
- Use configuration objects, not direct
process.envaccess - Type environment variables with TypeScript
// β
Good - Configuration management
// src/config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('7d'),
});
export const env = envSchema.parse(process.env);
// Usage
import { env } from './config/env';
const port = env.PORT;- Use different configs for dev, staging, production
- Use environment variables for secrets
- Use config files for non-sensitive settings
- Validate configuration on application startup
- Follow RESTful conventions
- Use proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Use proper HTTP status codes
- Use resource-based URLs
- Implement consistent response formats
- Use plural nouns for resources
// β
Good - RESTful API
GET /api/v1/users // List users
GET /api/v1/users/:id // Get user
POST /api/v1/users // Create user
PUT /api/v1/users/:id // Update user (full)
PATCH /api/v1/users/:id // Update user (partial)
DELETE /api/v1/users/:id // Delete user
// β Bad
GET /api/getUsers
POST /api/createUser
POST /api/updateUser
POST /api/deleteUser- Version APIs from the start (
/api/v1/) - Use URL versioning (
/api/v1/,/api/v2/) - Maintain backward compatibility when possible
- Document breaking changes
- Deprecate old versions gracefully
// β
Good - API versioning
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// Deprecation header
app.use('/api/v1', (req, res, next) => {
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Sunset', '2025-12-31');
next();
});- Use consistent response structure
- Include success/error indicators
- Provide meaningful error messages
- Include pagination metadata
- Use proper HTTP status codes
// β
Good - Consistent response format
// Success response
{
"success": true,
"data": {
"id": "123",
"name": "John Doe",
"email": "[email protected]"
}
}
// Error response
{
"success": false,
"error": {
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}
// Paginated response
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
}- Document all API endpoints
- Use OpenAPI/Swagger for API documentation
- Include request/response examples
- Document authentication requirements
- Include error response examples
- Keep documentation up to date
// β
Good - OpenAPI documentation
// Using swagger-jsdoc
/**
* @swagger
* /api/v1/users:
* get:
* summary: Get all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: Page number
* responses:
* 200:
* description: List of users
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/- Use CSS Modules or Tailwind CSS for most projects
- Use styled-components or Emotion for component-scoped styles
- Avoid global CSS when possible
- Use CSS variables for theming
- Follow BEM naming convention if using CSS Modules
// β
Good - CSS Modules
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary {
background-color: var(--color-primary);
}
// Button.tsx
import styles from './Button.module.css';
export function Button({ variant = 'primary' }: ButtonProps) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
Click me
</button>
);
}
// β
Good - Tailwind CSS
export function Button({ variant = 'primary' }: ButtonProps) {
return (
<button className="px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600">
Click me
</button>
);
}- Use mobile-first approach
- Use relative units (rem, em, %) over fixed units (px)
- Test on multiple screen sizes
- Use CSS Grid and Flexbox for layouts
- Implement proper breakpoints
// β
Good - Responsive design with Tailwind
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map(item => <Card key={item.id} item={item} />)}
</div>- Support light/dark themes
- Use CSS variables for theme values
- Provide theme switching functionality
- Ensure sufficient color contrast (WCAG AA)
- Follow WCAG 2.1 Level AA standards
- Ensure keyboard navigation works
- Provide proper ARIA labels
- Maintain proper heading hierarchy
- Ensure sufficient color contrast
- Provide alt text for images
// β
Good - Accessible component
export function Button({
label,
onClick,
disabled,
ariaLabel,
}: ButtonProps) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel || label}
className="btn btn-primary"
>
{label}
</button>
);
}
// β
Good - Accessible form
<form aria-label="User registration form">
<label htmlFor="email">
Email Address
<input
id="email"
type="email"
required
aria-describedby="email-error"
aria-invalid={hasError}
/>
{hasError && (
<span id="email-error" role="alert" className="error">
Please enter a valid email address
</span>
)}
</label>
</form>- Use semantic HTML elements (
<nav>,<main>,<article>,<section>) - Use proper heading hierarchy (h1 β h2 β h3)
- Use landmarks for page structure
- Ensure form labels are properly associated
- Use axe-core or jest-axe for automated testing
- Test with keyboard navigation
- Test with screen readers
- Use Lighthouse accessibility audit
- Use next-intl (Next.js) or react-i18next (React)
- Extract all user-facing strings
- Support multiple languages from the start
- Use proper locale formatting for dates, numbers, currencies
// β
Good - i18n with next-intl
// messages/en.json
{
"common": {
"welcome": "Welcome",
"submit": "Submit",
"cancel": "Cancel"
},
"users": {
"title": "Users",
"create": "Create User"
}
}
// Component
import { useTranslations } from 'next-intl';
export function UserForm() {
const t = useTranslations('users');
return (
<form>
<h1>{t('title')}</h1>
<button type="submit">{t('create')}</button>
</form>
);
}- Use locale-aware formatting
- Format dates according to user's locale
- Format numbers and currencies properly
- Handle timezones correctly
- Use Vite or Next.js built-in bundler
- Configure proper code splitting
- Optimize bundle size
- Use tree shaking
- Minimize production builds
// β
Good - Vite config
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
chunkSizeWarningLimit: 1000,
},
});- Split code by route
- Split vendor code separately
- Lazy load heavy components
- Use dynamic imports
// β
Good - Code splitting
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}- Use webpack-bundle-analyzer or rollup-plugin-visualizer
- Monitor bundle size
- Identify large dependencies
- Optimize imports
- Validate file types and sizes
- Use multer (Express) or formidable for file uploads
- Store files securely (cloud storage preferred)
- Generate unique filenames
- Sanitize filenames
- Implement virus scanning (production)
// β
Good - File upload with multer
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${uuidv4()}${ext}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(
path.extname(file.originalname).toLowerCase()
);
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'));
}
},
});
// Route
app.post('/api/upload', upload.single('image'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Process file...
});- Use AWS S3, Cloudinary, or Azure Blob Storage for production
- Generate signed URLs for secure access
- Implement CDN for file delivery
- Handle file deletion properly
- Use Bull (Redis-based) or Agenda (MongoDB-based) for job queues
- Process heavy operations asynchronously
- Implement retry logic with exponential backoff
- Monitor queue health
- Handle job failures gracefully
// β
Good - Bull queue setup
import Queue from 'bull';
import Redis from 'ioredis';
const emailQueue = new Queue('email', {
redis: {
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
},
});
// Add job
emailQueue.add('send-welcome-email', {
userId: user.id,
email: user.email,
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
// Process job
emailQueue.process('send-welcome-email', async (job) => {
const { userId, email } = job.data;
await emailService.sendWelcomeEmail(email);
});- Use queues for: email sending, image processing, data export, report generation
- Keep jobs idempotent when possible
- Implement job priorities
- Set appropriate timeouts
- Use Socket.io for real-time communication
- Implement proper authentication
- Handle connection errors gracefully
- Use rooms/namespaces for organization
- Implement rate limiting
// β
Good - Socket.io setup
import { Server } from 'socket.io';
import { Server as HttpServer } from 'http';
const httpServer = new HttpServer(app);
const io = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
credentials: true,
},
});
// Authentication middleware
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const user = await verifyToken(token);
socket.data.user = user;
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
// Connection handling
io.on('connection', (socket) => {
console.log(`User ${socket.data.user.id} connected`);
socket.on('join-room', (roomId) => {
socket.join(roomId);
});
socket.on('send-message', async (data) => {
// Save message to database
const message = await messageService.create(data);
// Broadcast to room
io.to(data.roomId).emit('new-message', message);
});
socket.on('disconnect', () => {
console.log(`User ${socket.data.user.id} disconnected`);
});
});- Use rooms for scoped communication
- Implement reconnection logic on client
- Handle connection state changes
- Limit message frequency
- Validate messages on server
- Use Nodemailer with SMTP or SendGrid/ AWS SES for production
- Use email templates (Handlebars, EJS)
- Implement email queuing
- Handle bounces and failures
- Track email delivery
// β
Good - Email service with Nodemailer
import nodemailer from 'nodemailer';
import { compile } from 'handlebars';
import fs from 'fs/promises';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export const emailService = {
async sendWelcomeEmail(user: User): Promise<void> {
const template = await fs.readFile('templates/welcome.hbs', 'utf-8');
const compiled = compile(template);
await transporter.sendMail({
from: process.env.FROM_EMAIL,
to: user.email,
subject: 'Welcome!',
html: compiled({ name: user.name }),
});
},
};- Use HTML templates with inline CSS
- Provide plain text alternatives
- Test across email clients
- Include unsubscribe links
- Follow email best practices
- Use Sentry for error tracking
- Use Datadog, New Relic, or Prometheus for metrics
- Monitor application performance (APM)
- Track key business metrics
- Set up alerts for critical issues
// β
Good - Sentry setup
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
// Error tracking
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { section: 'user-creation' },
extra: { userId: user.id },
});
throw error;
}- Implement health check endpoints
- Check database connectivity
- Check external service availability
- Return appropriate status codes
- Include version information
// β
Good - Health check endpoint
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
externalApi: await checkExternalApi(),
};
const isHealthy = Object.values(checks).every(check => check.status === 'ok');
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION,
});
});- Use structured logging
- Include correlation IDs
- Log at appropriate levels
- Don't log sensitive information
- Use log aggregation (ELK, CloudWatch)
- Run tests on every commit
- Run linting and type checking
- Run security audits
- Build and test in isolated environments
- Use GitHub Actions, GitLab CI, or CircleCI
# β
Good - GitHub Actions workflow
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm test
- run: npm audit- Use Docker for containerization
- Use Kubernetes or Docker Compose for orchestration
- Implement blue-green or canary deployments
- Use environment-specific configurations
- Automate deployments with CI/CD
# β
Good - Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]- Use separate environments (dev, staging, production)
- Manage secrets securely (Vault, AWS Secrets Manager)
- Use infrastructure as code (Terraform, CloudFormation)
- Document deployment procedures
- Use feature branches for new features
- Use main/master for production-ready code
- Use develop for integration (if using Git Flow)
- Keep branches short-lived
- Delete merged branches
- Use Conventional Commits format
- Write clear, descriptive messages
- Reference issues/tickets
- Keep commits atomic and focused
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
Examples:
feat(auth): add user registration endpoint
fix(api): resolve token validation issue
docs: update API documentation
test(api): add integration tests for auth
refactor(services): extract user validation logic
Every Pull Request must include the following checklist and all items must be satisfied before review approval.
- PR title is clear and descriptive
- PR scope is small and focused (single responsibility)
- Linked to ticket/task/issue
- No unrelated changes included
- Branch is up to date with main/master
- Code follows TypeScript/JavaScript standards
- No
anytypes without justification - No
@ts-ignorewithout explanation - Proper error handling implemented
- No duplicated logic
- No commented-out code
- Meaningful variable, function, and component names
- Consistent code style (ESLint, Prettier)
- Components are focused and reusable
- Business logic resides in Services
- Database access only via Repositories
- DTOs/types used instead of raw objects
- SOLID principles respected
- No forbidden anti-patterns introduced
- Proper separation of concerns
- Queries reviewed for N+1 issues
- Proper indexes added (if applicable)
- Transactions used for multi-step operations
- No unnecessary data fetching
- Caching considered where appropriate
- Bundle size impact considered (frontend)
- Tests written using Jest and React Testing Library
- New business logic has unit tests
- Feature tests updated/added
- Tests are deterministic (no flaky tests)
- Coverage does not decrease
- Edge cases tested
- ESLint passes
- Prettier formatting applied
- TypeScript compilation succeeds
- All tests pass
- CI pipeline is green
- No security vulnerabilities (
npm audit)
- No secrets committed
- Authorization checked (middleware, policies)
- Input validated (Zod schemas)
- Sensitive data not logged
- XSS/CSRF protections in place
- Rate limiting considered
- Code is self-documenting
- Complex logic has comments
- README updated if needed
- API documentation updated
- Type definitions are clear
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Related Issue
Closes #123
## Changes Made
- Change 1
- Change 2
- Change 3
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing completed
## Screenshots (if applicable)
[Add screenshots]
## Checklist
- [ ] Code follows project standards
- [ ] Tests pass
- [ ] Documentation updated
- [ ] No breaking changes (or documented)- Code follows TypeScript/JavaScript standards
- No
anytypes without justification - Proper error handling
- Tests written and passing
- No security vulnerabilities
- Performance considerations addressed
- Documentation updated
- No commented-out code
- Consistent code style
- Accessibility considered (frontend)
- Responsive design tested (frontend)
- Correctness: Does it work as intended?
- Security: Any vulnerabilities?
- Performance: Any bottlenecks?
- Maintainability: Is it easy to understand?
- Testing: Are edge cases covered?
- Documentation: Is it well-documented?
- Accessibility: Is it accessible?
- User Experience: Is the UX good?
Reviewers must evaluate PRs using the following rubric. Approval requires no Critical issues and all Major issues resolved.
| Level | Description |
|---|---|
| Critical | Must fix before merge |
| Major | Should fix before merge |
| Minor | Can fix later |
| Nit | Style or preference |
1. Correctness (Critical)
- Does the code do what the ticket describes?
- Are edge cases handled?
- Are error paths covered?
- Do tests cover the functionality?
2. Architecture & Design (Critical / Major)
- Proper separation of concerns
- No business logic in components/controllers
- Correct use of services, repositories, hooks
- Easy to extend without modification
- Follows established patterns
3. Readability & Maintainability (Major)
- Clear naming
- Small, focused functions/components
- Self-explanatory code
- Adequate comments where needed
- Consistent with codebase style
4. Performance & Scalability (Major)
- Efficient database access
- No unnecessary queries or renders
- Proper caching where appropriate
- Background jobs used for heavy operations
- Bundle size considered
5. Testing Quality (Critical)
- Tests exist and are meaningful
- Tests assert behavior, not implementation
- Edge cases tested
- Test coverage maintained or improved
6. Security (Critical)
- Proper authorization
- Input validation present
- No sensitive data exposure
- No XSS/CSRF vulnerabilities
- Secrets not committed
7. Consistency & Standards (Major)
- Conforms to this standards guide
- Matches existing patterns
- Tooling compliance (ESLint, Prettier, TypeScript)
- Follows naming conventions
8. Accessibility (Major - Frontend)
- WCAG compliance
- Keyboard navigation works
- Screen reader friendly
- Proper ARIA labels
| Condition | Decision |
|---|---|
| Any Critical issue | β Reject |
| Major issues present | π Changes requested |
| Only Minor/Nits | β Approve |
- Be constructive and specific
- Suggest improvements, not just problems
- Enforce standards consistently
- Focus on long-term maintainability
- Review within 24-48 hours when possible
- Approve promptly when standards are met
- Explain the "why" behind feedback
- Be respectful and professional
- Respond to all comments
- Push fixes promptly
- Do not dismiss feedback without justification
- Keep PRs up to date with main/master
- Request re-review after addressing feedback
- Be open to suggestions and improvements
- Ask questions if feedback is unclear
- Keep PRs focused and small
β Fat components - Components with business logic
β Business logic in API routes - Logic should be in services
β God objects - Classes/modules that do too much
β Tight coupling - Direct dependencies on concrete implementations
β Over-engineering - Using complex patterns for simple operations
β Prop drilling - Passing props through many levels
β Any types - Using any without justification
β Untyped functions - Functions without type annotations
β Magic numbers/strings - Hard-coded values
β Commented-out code - Dead code should be removed
β Copy-paste programming - Duplicated logic
β Large functions - Functions doing too much
β Mutating state directly - Always use setState/useState
β Using index as key - Use stable, unique keys
β Side effects in render - Use useEffect
β Creating objects/functions in render - Use useMemo/useCallback
β Unnecessary re-renders - Optimize with memoization
β N+1 queries - Use populate or aggregation
β Missing indexes - Index frequently queried fields
β Fetching unnecessary data - Use projection
β No pagination - Paginate large datasets
β Synchronous operations - Use async/await properly
β Secrets in code - Use environment variables
β No input validation - Validate all inputs
β SQL injection risks - Use parameterized queries
β XSS vulnerabilities - Sanitize user input
β No rate limiting - Implement rate limiting
β Weak passwords - Enforce strong passwords
If it is not tested, typed, documented, reviewed, and formatted β it is not production-ready.
- Linting: ESLint, Prettier
- Testing: Jest, React Testing Library, Supertest
- Type Checking: TypeScript
- API Testing: Postman, Insomnia
- Database Tools: MongoDB Compass
- Monitoring: Sentry, LogRocket
- Performance: Lighthouse, WebPageTest