Skills added: - cloudflare-workers-expert: For servers-api Worker and edge computing - n8n-workflow-patterns: For self-hosted n8n on Command Center - nodejs-backend-patterns: For Arbiter Express backend work - git-advanced-workflows: Git learning (rebase, cherry-pick, recovery) All sourced from antigravity-skills-reference (MIT license) Chronicler #73
1020 lines
24 KiB
Markdown
1020 lines
24 KiB
Markdown
# Node.js Backend Patterns Implementation Playbook
|
|
|
|
This file contains detailed patterns, checklists, and code samples referenced by the skill.
|
|
|
|
# Node.js Backend Patterns
|
|
|
|
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Building REST APIs or GraphQL servers
|
|
- Creating microservices with Node.js
|
|
- Implementing authentication and authorization
|
|
- Designing scalable backend architectures
|
|
- Setting up middleware and error handling
|
|
- Integrating databases (SQL and NoSQL)
|
|
- Building real-time applications with WebSockets
|
|
- Implementing background job processing
|
|
|
|
## Core Frameworks
|
|
|
|
### Express.js - Minimalist Framework
|
|
|
|
**Basic Setup:**
|
|
```typescript
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import helmet from 'helmet';
|
|
import cors from 'cors';
|
|
import compression from 'compression';
|
|
|
|
const app = express();
|
|
|
|
// Security middleware
|
|
app.use(helmet());
|
|
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
|
|
app.use(compression());
|
|
|
|
// Body parsing
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Request logging
|
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
console.log(`${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
```
|
|
|
|
### Fastify - High Performance Framework
|
|
|
|
**Basic Setup:**
|
|
```typescript
|
|
import Fastify from 'fastify';
|
|
import helmet from '@fastify/helmet';
|
|
import cors from '@fastify/cors';
|
|
import compress from '@fastify/compress';
|
|
|
|
const fastify = Fastify({
|
|
logger: {
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
transport: {
|
|
target: 'pino-pretty',
|
|
options: { colorize: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Plugins
|
|
await fastify.register(helmet);
|
|
await fastify.register(cors, { origin: true });
|
|
await fastify.register(compress);
|
|
|
|
// Type-safe routes with schema validation
|
|
fastify.post<{
|
|
Body: { name: string; email: string };
|
|
Reply: { id: string; name: string };
|
|
}>('/users', {
|
|
schema: {
|
|
body: {
|
|
type: 'object',
|
|
required: ['name', 'email'],
|
|
properties: {
|
|
name: { type: 'string', minLength: 1 },
|
|
email: { type: 'string', format: 'email' }
|
|
}
|
|
}
|
|
}
|
|
}, async (request, reply) => {
|
|
const { name, email } = request.body;
|
|
return { id: '123', name };
|
|
});
|
|
|
|
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
|
```
|
|
|
|
## Architectural Patterns
|
|
|
|
### Pattern 1: Layered Architecture
|
|
|
|
**Structure:**
|
|
```
|
|
src/
|
|
├── controllers/ # Handle HTTP requests/responses
|
|
├── services/ # Business logic
|
|
├── repositories/ # Data access layer
|
|
├── models/ # Data models
|
|
├── middleware/ # Express/Fastify middleware
|
|
├── routes/ # Route definitions
|
|
├── utils/ # Helper functions
|
|
├── config/ # Configuration
|
|
└── types/ # TypeScript types
|
|
```
|
|
|
|
**Controller Layer:**
|
|
```typescript
|
|
// controllers/user.controller.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { UserService } from '../services/user.service';
|
|
import { CreateUserDTO, UpdateUserDTO } from '../types/user.types';
|
|
|
|
export class UserController {
|
|
constructor(private userService: UserService) {}
|
|
|
|
async createUser(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const userData: CreateUserDTO = req.body;
|
|
const user = await this.userService.createUser(userData);
|
|
res.status(201).json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getUser(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const { id } = req.params;
|
|
const user = await this.userService.getUserById(id);
|
|
res.json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateUser(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates: UpdateUserDTO = req.body;
|
|
const user = await this.userService.updateUser(id, updates);
|
|
res.json(user);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteUser(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const { id } = req.params;
|
|
await this.userService.deleteUser(id);
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Service Layer:**
|
|
```typescript
|
|
// services/user.service.ts
|
|
import { UserRepository } from '../repositories/user.repository';
|
|
import { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types';
|
|
import { NotFoundError, ValidationError } from '../utils/errors';
|
|
import bcrypt from 'bcrypt';
|
|
|
|
export class UserService {
|
|
constructor(private userRepository: UserRepository) {}
|
|
|
|
async createUser(userData: CreateUserDTO): Promise<User> {
|
|
// Validation
|
|
const existingUser = await this.userRepository.findByEmail(userData.email);
|
|
if (existingUser) {
|
|
throw new ValidationError('Email already exists');
|
|
}
|
|
|
|
// Hash password
|
|
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
|
|
|
// Create user
|
|
const user = await this.userRepository.create({
|
|
...userData,
|
|
password: hashedPassword
|
|
});
|
|
|
|
// Remove password from response
|
|
const { password, ...userWithoutPassword } = user;
|
|
return userWithoutPassword as User;
|
|
}
|
|
|
|
async getUserById(id: string): Promise<User> {
|
|
const user = await this.userRepository.findById(id);
|
|
if (!user) {
|
|
throw new NotFoundError('User not found');
|
|
}
|
|
const { password, ...userWithoutPassword } = user;
|
|
return userWithoutPassword as User;
|
|
}
|
|
|
|
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
|
const user = await this.userRepository.update(id, updates);
|
|
if (!user) {
|
|
throw new NotFoundError('User not found');
|
|
}
|
|
const { password, ...userWithoutPassword } = user;
|
|
return userWithoutPassword as User;
|
|
}
|
|
|
|
async deleteUser(id: string): Promise<void> {
|
|
const deleted = await this.userRepository.delete(id);
|
|
if (!deleted) {
|
|
throw new NotFoundError('User not found');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Repository Layer:**
|
|
```typescript
|
|
// repositories/user.repository.ts
|
|
import { Pool } from 'pg';
|
|
import { CreateUserDTO, UpdateUserDTO, UserEntity } from '../types/user.types';
|
|
|
|
export class UserRepository {
|
|
constructor(private db: Pool) {}
|
|
|
|
async create(userData: CreateUserDTO & { password: string }): Promise<UserEntity> {
|
|
const query = `
|
|
INSERT INTO users (name, email, password)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, name, email, password, created_at, updated_at
|
|
`;
|
|
const { rows } = await this.db.query(query, [
|
|
userData.name,
|
|
userData.email,
|
|
userData.password
|
|
]);
|
|
return rows[0];
|
|
}
|
|
|
|
async findById(id: string): Promise<UserEntity | null> {
|
|
const query = 'SELECT * FROM users WHERE id = $1';
|
|
const { rows } = await this.db.query(query, [id]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
async findByEmail(email: string): Promise<UserEntity | null> {
|
|
const query = 'SELECT * FROM users WHERE email = $1';
|
|
const { rows } = await this.db.query(query, [email]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
|
|
const fields = Object.keys(updates);
|
|
const values = Object.values(updates);
|
|
|
|
const setClause = fields
|
|
.map((field, idx) => `${field} = $${idx + 2}`)
|
|
.join(', ');
|
|
|
|
const query = `
|
|
UPDATE users
|
|
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
RETURNING *
|
|
`;
|
|
|
|
const { rows } = await this.db.query(query, [id, ...values]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
async delete(id: string): Promise<boolean> {
|
|
const query = 'DELETE FROM users WHERE id = $1';
|
|
const { rowCount } = await this.db.query(query, [id]);
|
|
return rowCount > 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Dependency Injection
|
|
|
|
**DI Container:**
|
|
```typescript
|
|
// di-container.ts
|
|
import { Pool } from 'pg';
|
|
import { UserRepository } from './repositories/user.repository';
|
|
import { UserService } from './services/user.service';
|
|
import { UserController } from './controllers/user.controller';
|
|
import { AuthService } from './services/auth.service';
|
|
|
|
class Container {
|
|
private instances = new Map<string, any>();
|
|
|
|
register<T>(key: string, factory: () => T): void {
|
|
this.instances.set(key, factory);
|
|
}
|
|
|
|
resolve<T>(key: string): T {
|
|
const factory = this.instances.get(key);
|
|
if (!factory) {
|
|
throw new Error(`No factory registered for ${key}`);
|
|
}
|
|
return factory();
|
|
}
|
|
|
|
singleton<T>(key: string, factory: () => T): void {
|
|
let instance: T;
|
|
this.instances.set(key, () => {
|
|
if (!instance) {
|
|
instance = factory();
|
|
}
|
|
return instance;
|
|
});
|
|
}
|
|
}
|
|
|
|
export const container = new Container();
|
|
|
|
// Register dependencies
|
|
container.singleton('db', () => new Pool({
|
|
host: process.env.DB_HOST,
|
|
port: parseInt(process.env.DB_PORT || '5432'),
|
|
database: process.env.DB_NAME,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
max: 20,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 2000,
|
|
}));
|
|
|
|
container.singleton('userRepository', () =>
|
|
new UserRepository(container.resolve('db'))
|
|
);
|
|
|
|
container.singleton('userService', () =>
|
|
new UserService(container.resolve('userRepository'))
|
|
);
|
|
|
|
container.register('userController', () =>
|
|
new UserController(container.resolve('userService'))
|
|
);
|
|
|
|
container.singleton('authService', () =>
|
|
new AuthService(container.resolve('userRepository'))
|
|
);
|
|
```
|
|
|
|
## Middleware Patterns
|
|
|
|
### Authentication Middleware
|
|
|
|
```typescript
|
|
// middleware/auth.middleware.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import { UnauthorizedError } from '../utils/errors';
|
|
|
|
interface JWTPayload {
|
|
userId: string;
|
|
email: string;
|
|
}
|
|
|
|
declare global {
|
|
namespace Express {
|
|
interface Request {
|
|
user?: JWTPayload;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const authenticate = async (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
try {
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
|
|
if (!token) {
|
|
throw new UnauthorizedError('No token provided');
|
|
}
|
|
|
|
const payload = jwt.verify(
|
|
token,
|
|
process.env.JWT_SECRET!
|
|
) as JWTPayload;
|
|
|
|
req.user = payload;
|
|
next();
|
|
} catch (error) {
|
|
next(new UnauthorizedError('Invalid token'));
|
|
}
|
|
};
|
|
|
|
export const authorize = (...roles: string[]) => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return next(new UnauthorizedError('Not authenticated'));
|
|
}
|
|
|
|
// Check if user has required role
|
|
const hasRole = roles.some(role =>
|
|
req.user?.roles?.includes(role)
|
|
);
|
|
|
|
if (!hasRole) {
|
|
return next(new UnauthorizedError('Insufficient permissions'));
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
```
|
|
|
|
### Validation Middleware
|
|
|
|
```typescript
|
|
// middleware/validation.middleware.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { AnyZodObject, ZodError } from 'zod';
|
|
import { ValidationError } from '../utils/errors';
|
|
|
|
export const validate = (schema: AnyZodObject) => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
await schema.parseAsync({
|
|
body: req.body,
|
|
query: req.query,
|
|
params: req.params
|
|
});
|
|
next();
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
const errors = error.errors.map(err => ({
|
|
field: err.path.join('.'),
|
|
message: err.message
|
|
}));
|
|
next(new ValidationError('Validation failed', errors));
|
|
} else {
|
|
next(error);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// Usage with Zod
|
|
import { z } from 'zod';
|
|
|
|
const createUserSchema = z.object({
|
|
body: z.object({
|
|
name: z.string().min(1),
|
|
email: z.string().email(),
|
|
password: z.string().min(8)
|
|
})
|
|
});
|
|
|
|
router.post('/users', validate(createUserSchema), userController.createUser);
|
|
```
|
|
|
|
### Rate Limiting Middleware
|
|
|
|
```typescript
|
|
// middleware/rate-limit.middleware.ts
|
|
import rateLimit from 'express-rate-limit';
|
|
import RedisStore from 'rate-limit-redis';
|
|
import Redis from 'ioredis';
|
|
|
|
const redis = new Redis({
|
|
host: process.env.REDIS_HOST,
|
|
port: parseInt(process.env.REDIS_PORT || '6379')
|
|
});
|
|
|
|
export const apiLimiter = rateLimit({
|
|
store: new RedisStore({
|
|
client: redis,
|
|
prefix: 'rl:',
|
|
}),
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 100, // Limit each IP to 100 requests per windowMs
|
|
message: 'Too many requests from this IP, please try again later',
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
});
|
|
|
|
export const authLimiter = rateLimit({
|
|
store: new RedisStore({
|
|
client: redis,
|
|
prefix: 'rl:auth:',
|
|
}),
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 5, // Stricter limit for auth endpoints
|
|
skipSuccessfulRequests: true,
|
|
});
|
|
```
|
|
|
|
### Request Logging Middleware
|
|
|
|
```typescript
|
|
// middleware/logger.middleware.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import pino from 'pino';
|
|
|
|
const logger = pino({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
transport: {
|
|
target: 'pino-pretty',
|
|
options: { colorize: true }
|
|
}
|
|
});
|
|
|
|
export const requestLogger = (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
const start = Date.now();
|
|
|
|
// Log response when finished
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
logger.info({
|
|
method: req.method,
|
|
url: req.url,
|
|
status: res.statusCode,
|
|
duration: `${duration}ms`,
|
|
userAgent: req.headers['user-agent'],
|
|
ip: req.ip
|
|
});
|
|
});
|
|
|
|
next();
|
|
};
|
|
|
|
export { logger };
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Custom Error Classes
|
|
|
|
```typescript
|
|
// utils/errors.ts
|
|
export class AppError extends Error {
|
|
constructor(
|
|
public message: string,
|
|
public statusCode: number = 500,
|
|
public isOperational: boolean = true
|
|
) {
|
|
super(message);
|
|
Object.setPrototypeOf(this, AppError.prototype);
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
}
|
|
|
|
export class ValidationError extends AppError {
|
|
constructor(message: string, public errors?: any[]) {
|
|
super(message, 400);
|
|
}
|
|
}
|
|
|
|
export class NotFoundError extends AppError {
|
|
constructor(message: string = 'Resource not found') {
|
|
super(message, 404);
|
|
}
|
|
}
|
|
|
|
export class UnauthorizedError extends AppError {
|
|
constructor(message: string = 'Unauthorized') {
|
|
super(message, 401);
|
|
}
|
|
}
|
|
|
|
export class ForbiddenError extends AppError {
|
|
constructor(message: string = 'Forbidden') {
|
|
super(message, 403);
|
|
}
|
|
}
|
|
|
|
export class ConflictError extends AppError {
|
|
constructor(message: string) {
|
|
super(message, 409);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Global Error Handler
|
|
|
|
```typescript
|
|
// middleware/error-handler.ts
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { AppError } from '../utils/errors';
|
|
import { logger } from './logger.middleware';
|
|
|
|
export const errorHandler = (
|
|
err: Error,
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (err instanceof AppError) {
|
|
return res.status(err.statusCode).json({
|
|
status: 'error',
|
|
message: err.message,
|
|
...(err instanceof ValidationError && { errors: err.errors })
|
|
});
|
|
}
|
|
|
|
// Log unexpected errors
|
|
logger.error({
|
|
error: err.message,
|
|
stack: err.stack,
|
|
url: req.url,
|
|
method: req.method
|
|
});
|
|
|
|
// Don't leak error details in production
|
|
const message = process.env.NODE_ENV === 'production'
|
|
? 'Internal server error'
|
|
: err.message;
|
|
|
|
res.status(500).json({
|
|
status: 'error',
|
|
message
|
|
});
|
|
};
|
|
|
|
// Async error wrapper
|
|
export const asyncHandler = (
|
|
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
|
|
) => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
};
|
|
};
|
|
```
|
|
|
|
## Database Patterns
|
|
|
|
### PostgreSQL with Connection Pool
|
|
|
|
```typescript
|
|
// config/database.ts
|
|
import { Pool, PoolConfig } from 'pg';
|
|
|
|
const poolConfig: PoolConfig = {
|
|
host: process.env.DB_HOST,
|
|
port: parseInt(process.env.DB_PORT || '5432'),
|
|
database: process.env.DB_NAME,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
max: 20,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 2000,
|
|
};
|
|
|
|
export const pool = new Pool(poolConfig);
|
|
|
|
// Test connection
|
|
pool.on('connect', () => {
|
|
console.log('Database connected');
|
|
});
|
|
|
|
pool.on('error', (err) => {
|
|
console.error('Unexpected database error', err);
|
|
process.exit(-1);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
export const closeDatabase = async () => {
|
|
await pool.end();
|
|
console.log('Database connection closed');
|
|
};
|
|
```
|
|
|
|
### MongoDB with Mongoose
|
|
|
|
```typescript
|
|
// config/mongoose.ts
|
|
import mongoose from 'mongoose';
|
|
|
|
const connectDB = async () => {
|
|
try {
|
|
await mongoose.connect(process.env.MONGODB_URI!, {
|
|
maxPoolSize: 10,
|
|
serverSelectionTimeoutMS: 5000,
|
|
socketTimeoutMS: 45000,
|
|
});
|
|
|
|
console.log('MongoDB connected');
|
|
} catch (error) {
|
|
console.error('MongoDB connection error:', error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
mongoose.connection.on('disconnected', () => {
|
|
console.log('MongoDB disconnected');
|
|
});
|
|
|
|
mongoose.connection.on('error', (err) => {
|
|
console.error('MongoDB error:', err);
|
|
});
|
|
|
|
export { connectDB };
|
|
|
|
// Model example
|
|
import { Schema, model, Document } from 'mongoose';
|
|
|
|
interface IUser extends Document {
|
|
name: string;
|
|
email: string;
|
|
password: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
const userSchema = new Schema<IUser>({
|
|
name: { type: String, required: true },
|
|
email: { type: String, required: true, unique: true },
|
|
password: { type: String, required: true },
|
|
}, {
|
|
timestamps: true
|
|
});
|
|
|
|
// Indexes
|
|
userSchema.index({ email: 1 });
|
|
|
|
export const User = model<IUser>('User', userSchema);
|
|
```
|
|
|
|
### Transaction Pattern
|
|
|
|
```typescript
|
|
// services/order.service.ts
|
|
import { Pool } from 'pg';
|
|
|
|
export class OrderService {
|
|
constructor(private db: Pool) {}
|
|
|
|
async createOrder(userId: string, items: any[]) {
|
|
const client = await this.db.connect();
|
|
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// Create order
|
|
const orderResult = await client.query(
|
|
'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
|
|
[userId, calculateTotal(items)]
|
|
);
|
|
const orderId = orderResult.rows[0].id;
|
|
|
|
// Create order items
|
|
for (const item of items) {
|
|
await client.query(
|
|
'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)',
|
|
[orderId, item.productId, item.quantity, item.price]
|
|
);
|
|
|
|
// Update inventory
|
|
await client.query(
|
|
'UPDATE products SET stock = stock - $1 WHERE id = $2',
|
|
[item.quantity, item.productId]
|
|
);
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
return orderId;
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authentication & Authorization
|
|
|
|
### JWT Authentication
|
|
|
|
```typescript
|
|
// services/auth.service.ts
|
|
import jwt from 'jsonwebtoken';
|
|
import bcrypt from 'bcrypt';
|
|
import { UserRepository } from '../repositories/user.repository';
|
|
import { UnauthorizedError } from '../utils/errors';
|
|
|
|
export class AuthService {
|
|
constructor(private userRepository: UserRepository) {}
|
|
|
|
async login(email: string, password: string) {
|
|
const user = await this.userRepository.findByEmail(email);
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedError('Invalid credentials');
|
|
}
|
|
|
|
const isValid = await bcrypt.compare(password, user.password);
|
|
|
|
if (!isValid) {
|
|
throw new UnauthorizedError('Invalid credentials');
|
|
}
|
|
|
|
const token = this.generateToken({
|
|
userId: user.id,
|
|
email: user.email
|
|
});
|
|
|
|
const refreshToken = this.generateRefreshToken({
|
|
userId: user.id
|
|
});
|
|
|
|
return {
|
|
token,
|
|
refreshToken,
|
|
user: {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email
|
|
}
|
|
};
|
|
}
|
|
|
|
async refreshToken(refreshToken: string) {
|
|
try {
|
|
const payload = jwt.verify(
|
|
refreshToken,
|
|
process.env.REFRESH_TOKEN_SECRET!
|
|
) as { userId: string };
|
|
|
|
const user = await this.userRepository.findById(payload.userId);
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedError('User not found');
|
|
}
|
|
|
|
const token = this.generateToken({
|
|
userId: user.id,
|
|
email: user.email
|
|
});
|
|
|
|
return { token };
|
|
} catch (error) {
|
|
throw new UnauthorizedError('Invalid refresh token');
|
|
}
|
|
}
|
|
|
|
private generateToken(payload: any): string {
|
|
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
|
expiresIn: '15m'
|
|
});
|
|
}
|
|
|
|
private generateRefreshToken(payload: any): string {
|
|
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
|
expiresIn: '7d'
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Caching Strategies
|
|
|
|
```typescript
|
|
// utils/cache.ts
|
|
import Redis from 'ioredis';
|
|
|
|
const redis = new Redis({
|
|
host: process.env.REDIS_HOST,
|
|
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
retryStrategy: (times) => {
|
|
const delay = Math.min(times * 50, 2000);
|
|
return delay;
|
|
}
|
|
});
|
|
|
|
export class CacheService {
|
|
async get<T>(key: string): Promise<T | null> {
|
|
const data = await redis.get(key);
|
|
return data ? JSON.parse(data) : null;
|
|
}
|
|
|
|
async set(key: string, value: any, ttl?: number): Promise<void> {
|
|
const serialized = JSON.stringify(value);
|
|
if (ttl) {
|
|
await redis.setex(key, ttl, serialized);
|
|
} else {
|
|
await redis.set(key, serialized);
|
|
}
|
|
}
|
|
|
|
async delete(key: string): Promise<void> {
|
|
await redis.del(key);
|
|
}
|
|
|
|
async invalidatePattern(pattern: string): Promise<void> {
|
|
const keys = await redis.keys(pattern);
|
|
if (keys.length > 0) {
|
|
await redis.del(...keys);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache decorator
|
|
export function Cacheable(ttl: number = 300) {
|
|
return function (
|
|
target: any,
|
|
propertyKey: string,
|
|
descriptor: PropertyDescriptor
|
|
) {
|
|
const originalMethod = descriptor.value;
|
|
|
|
descriptor.value = async function (...args: any[]) {
|
|
const cache = new CacheService();
|
|
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
|
|
|
const cached = await cache.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const result = await originalMethod.apply(this, args);
|
|
await cache.set(cacheKey, result, ttl);
|
|
|
|
return result;
|
|
};
|
|
|
|
return descriptor;
|
|
};
|
|
}
|
|
```
|
|
|
|
## API Response Format
|
|
|
|
```typescript
|
|
// utils/response.ts
|
|
import { Response } from 'express';
|
|
|
|
export class ApiResponse {
|
|
static success<T>(res: Response, data: T, message?: string, statusCode = 200) {
|
|
return res.status(statusCode).json({
|
|
status: 'success',
|
|
message,
|
|
data
|
|
});
|
|
}
|
|
|
|
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
|
return res.status(statusCode).json({
|
|
status: 'error',
|
|
message,
|
|
...(errors && { errors })
|
|
});
|
|
}
|
|
|
|
static paginated<T>(
|
|
res: Response,
|
|
data: T[],
|
|
page: number,
|
|
limit: number,
|
|
total: number
|
|
) {
|
|
return res.json({
|
|
status: 'success',
|
|
data,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
pages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Use TypeScript**: Type safety prevents runtime errors
|
|
2. **Implement proper error handling**: Use custom error classes
|
|
3. **Validate input**: Use libraries like Zod or Joi
|
|
4. **Use environment variables**: Never hardcode secrets
|
|
5. **Implement logging**: Use structured logging (Pino, Winston)
|
|
6. **Add rate limiting**: Prevent abuse
|
|
7. **Use HTTPS**: Always in production
|
|
8. **Implement CORS properly**: Don't use `*` in production
|
|
9. **Use dependency injection**: Easier testing and maintenance
|
|
10. **Write tests**: Unit, integration, and E2E tests
|
|
11. **Handle graceful shutdown**: Clean up resources
|
|
12. **Use connection pooling**: For databases
|
|
13. **Implement health checks**: For monitoring
|
|
14. **Use compression**: Reduce response size
|
|
15. **Monitor performance**: Use APM tools
|
|
|
|
## Testing Patterns
|
|
|
|
See `javascript-testing-patterns` skill for comprehensive testing guidance.
|
|
|
|
## Resources
|
|
|
|
- **Node.js Best Practices**: https://github.com/goldbergyoni/nodebestpractices
|
|
- **Express.js Guide**: https://expressjs.com/en/guide/
|
|
- **Fastify Documentation**: https://www.fastify.io/docs/
|
|
- **TypeScript Node Starter**: https://github.com/microsoft/TypeScript-Node-Starter
|