Backend Development Patterns
Backend architecture patterns and best practices for scalable server-side applications.
When to Activate
- Designing REST or GraphQL API endpoints
- Implementing repository, service, or controller layers
- Optimizing database queries (N+1, indexing, connection pooling)
- Adding caching (Redis, in-memory, HTTP cache headers)
- Setting up background jobs or async processing
- Structuring error handling and validation for APIs
- Building middleware (auth, logging, rate limiting)
API Design Patterns
RESTful API Structure
typescript1// ✅ Resource-based URLs 2GET /api/markets # List resources 3GET /api/markets/:id # Get single resource 4POST /api/markets # Create resource 5PUT /api/markets/:id # Replace resource 6PATCH /api/markets/:id # Update resource 7DELETE /api/markets/:id # Delete resource 8 9// ✅ Query parameters for filtering, sorting, pagination 10GET /api/markets?status=active&sort=volume&limit=20&offset=0
Repository Pattern
typescript1// Abstract data access logic 2interface MarketRepository { 3 findAll(filters?: MarketFilters): Promise<Market[]> 4 findById(id: string): Promise<Market | null> 5 create(data: CreateMarketDto): Promise<Market> 6 update(id: string, data: UpdateMarketDto): Promise<Market> 7 delete(id: string): Promise<void> 8} 9 10class SupabaseMarketRepository implements MarketRepository { 11 async findAll(filters?: MarketFilters): Promise<Market[]> { 12 let query = supabase.from('markets').select('*') 13 14 if (filters?.status) { 15 query = query.eq('status', filters.status) 16 } 17 18 if (filters?.limit) { 19 query = query.limit(filters.limit) 20 } 21 22 const { data, error } = await query 23 24 if (error) throw new Error(error.message) 25 return data 26 } 27 28 // Other methods... 29}
Service Layer Pattern
typescript1// Business logic separated from data access 2class MarketService { 3 constructor(private marketRepo: MarketRepository) {} 4 5 async searchMarkets(query: string, limit: number = 10): Promise<Market[]> { 6 // Business logic 7 const embedding = await generateEmbedding(query) 8 const results = await this.vectorSearch(embedding, limit) 9 10 // Fetch full data 11 const markets = await this.marketRepo.findByIds(results.map(r => r.id)) 12 13 // Sort by similarity 14 return markets.sort((a, b) => { 15 const scoreA = results.find(r => r.id === a.id)?.score || 0 16 const scoreB = results.find(r => r.id === b.id)?.score || 0 17 return scoreA - scoreB 18 }) 19 } 20 21 private async vectorSearch(embedding: number[], limit: number) { 22 // Vector search implementation 23 } 24}
Middleware Pattern
typescript1// Request/response processing pipeline 2export function withAuth(handler: NextApiHandler): NextApiHandler { 3 return async (req, res) => { 4 const token = req.headers.authorization?.replace('Bearer ', '') 5 6 if (!token) { 7 return res.status(401).json({ error: 'Unauthorized' }) 8 } 9 10 try { 11 const user = await verifyToken(token) 12 req.user = user 13 return handler(req, res) 14 } catch (error) { 15 return res.status(401).json({ error: 'Invalid token' }) 16 } 17 } 18} 19 20// Usage 21export default withAuth(async (req, res) => { 22 // Handler has access to req.user 23})
Database Patterns
Query Optimization
typescript1// ✅ GOOD: Select only needed columns 2const { data } = await supabase 3 .from('markets') 4 .select('id, name, status, volume') 5 .eq('status', 'active') 6 .order('volume', { ascending: false }) 7 .limit(10) 8 9// ❌ BAD: Select everything 10const { data } = await supabase 11 .from('markets') 12 .select('*')
N+1 Query Prevention
typescript1// ❌ BAD: N+1 query problem 2const markets = await getMarkets() 3for (const market of markets) { 4 market.creator = await getUser(market.creator_id) // N queries 5} 6 7// ✅ GOOD: Batch fetch 8const markets = await getMarkets() 9const creatorIds = markets.map(m => m.creator_id) 10const creators = await getUsers(creatorIds) // 1 query 11const creatorMap = new Map(creators.map(c => [c.id, c])) 12 13markets.forEach(market => { 14 market.creator = creatorMap.get(market.creator_id) 15})
Transaction Pattern
typescript1async function createMarketWithPosition( 2 marketData: CreateMarketDto, 3 positionData: CreatePositionDto 4) { 5 // Use Supabase transaction 6 const { data, error } = await supabase.rpc('create_market_with_position', { 7 market_data: marketData, 8 position_data: positionData 9 }) 10 11 if (error) throw new Error('Transaction failed') 12 return data 13} 14 15// SQL function in Supabase 16CREATE OR REPLACE FUNCTION create_market_with_position( 17 market_data jsonb, 18 position_data jsonb 19) 20RETURNS jsonb 21LANGUAGE plpgsql 22AS $$ 23BEGIN 24 -- Start transaction automatically 25 INSERT INTO markets VALUES (market_data); 26 INSERT INTO positions VALUES (position_data); 27 RETURN jsonb_build_object('success', true); 28EXCEPTION 29 WHEN OTHERS THEN 30 -- Rollback happens automatically 31 RETURN jsonb_build_object('success', false, 'error', SQLERRM); 32END; 33$$;
Caching Strategies
Redis Caching Layer
typescript1class CachedMarketRepository implements MarketRepository { 2 constructor( 3 private baseRepo: MarketRepository, 4 private redis: RedisClient 5 ) {} 6 7 async findById(id: string): Promise<Market | null> { 8 // Check cache first 9 const cached = await this.redis.get(`market:${id}`) 10 11 if (cached) { 12 return JSON.parse(cached) 13 } 14 15 // Cache miss - fetch from database 16 const market = await this.baseRepo.findById(id) 17 18 if (market) { 19 // Cache for 5 minutes 20 await this.redis.setex(`market:${id}`, 300, JSON.stringify(market)) 21 } 22 23 return market 24 } 25 26 async invalidateCache(id: string): Promise<void> { 27 await this.redis.del(`market:${id}`) 28 } 29}
Cache-Aside Pattern
typescript1async function getMarketWithCache(id: string): Promise<Market> { 2 const cacheKey = `market:${id}` 3 4 // Try cache 5 const cached = await redis.get(cacheKey) 6 if (cached) return JSON.parse(cached) 7 8 // Cache miss - fetch from DB 9 const market = await db.markets.findUnique({ where: { id } }) 10 11 if (!market) throw new Error('Market not found') 12 13 // Update cache 14 await redis.setex(cacheKey, 300, JSON.stringify(market)) 15 16 return market 17}
Error Handling Patterns
Centralized Error Handler
typescript1class ApiError extends Error { 2 constructor( 3 public statusCode: number, 4 public message: string, 5 public isOperational = true 6 ) { 7 super(message) 8 Object.setPrototypeOf(this, ApiError.prototype) 9 } 10} 11 12export function errorHandler(error: unknown, req: Request): Response { 13 if (error instanceof ApiError) { 14 return NextResponse.json({ 15 success: false, 16 error: error.message 17 }, { status: error.statusCode }) 18 } 19 20 if (error instanceof z.ZodError) { 21 return NextResponse.json({ 22 success: false, 23 error: 'Validation failed', 24 details: error.errors 25 }, { status: 400 }) 26 } 27 28 // Log unexpected errors 29 console.error('Unexpected error:', error) 30 31 return NextResponse.json({ 32 success: false, 33 error: 'Internal server error' 34 }, { status: 500 }) 35} 36 37// Usage 38export async function GET(request: Request) { 39 try { 40 const data = await fetchData() 41 return NextResponse.json({ success: true, data }) 42 } catch (error) { 43 return errorHandler(error, request) 44 } 45}
Retry with Exponential Backoff
typescript1async function fetchWithRetry<T>( 2 fn: () => Promise<T>, 3 maxRetries = 3 4): Promise<T> { 5 let lastError: Error 6 7 for (let i = 0; i < maxRetries; i++) { 8 try { 9 return await fn() 10 } catch (error) { 11 lastError = error as Error 12 13 if (i < maxRetries - 1) { 14 // Exponential backoff: 1s, 2s, 4s 15 const delay = Math.pow(2, i) * 1000 16 await new Promise(resolve => setTimeout(resolve, delay)) 17 } 18 } 19 } 20 21 throw lastError! 22} 23 24// Usage 25const data = await fetchWithRetry(() => fetchFromAPI())
Authentication & Authorization
JWT Token Validation
typescript1import jwt from 'jsonwebtoken' 2 3interface JWTPayload { 4 userId: string 5 email: string 6 role: 'admin' | 'user' 7} 8 9export function verifyToken(token: string): JWTPayload { 10 try { 11 const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload 12 return payload 13 } catch (error) { 14 throw new ApiError(401, 'Invalid token') 15 } 16} 17 18export async function requireAuth(request: Request) { 19 const token = request.headers.get('authorization')?.replace('Bearer ', '') 20 21 if (!token) { 22 throw new ApiError(401, 'Missing authorization token') 23 } 24 25 return verifyToken(token) 26} 27 28// Usage in API route 29export async function GET(request: Request) { 30 const user = await requireAuth(request) 31 32 const data = await getDataForUser(user.userId) 33 34 return NextResponse.json({ success: true, data }) 35}
Role-Based Access Control
typescript1type Permission = 'read' | 'write' | 'delete' | 'admin' 2 3interface User { 4 id: string 5 role: 'admin' | 'moderator' | 'user' 6} 7 8const rolePermissions: Record<User['role'], Permission[]> = { 9 admin: ['read', 'write', 'delete', 'admin'], 10 moderator: ['read', 'write', 'delete'], 11 user: ['read', 'write'] 12} 13 14export function hasPermission(user: User, permission: Permission): boolean { 15 return rolePermissions[user.role].includes(permission) 16} 17 18export function requirePermission(permission: Permission) { 19 return (handler: (request: Request, user: User) => Promise<Response>) => { 20 return async (request: Request) => { 21 const user = await requireAuth(request) 22 23 if (!hasPermission(user, permission)) { 24 throw new ApiError(403, 'Insufficient permissions') 25 } 26 27 return handler(request, user) 28 } 29 } 30} 31 32// Usage - HOF wraps the handler 33export const DELETE = requirePermission('delete')( 34 async (request: Request, user: User) => { 35 // Handler receives authenticated user with verified permission 36 return new Response('Deleted', { status: 200 }) 37 } 38)
Rate Limiting
Simple In-Memory Rate Limiter
typescript1class RateLimiter { 2 private requests = new Map<string, number[]>() 3 4 async checkLimit( 5 identifier: string, 6 maxRequests: number, 7 windowMs: number 8 ): Promise<boolean> { 9 const now = Date.now() 10 const requests = this.requests.get(identifier) || [] 11 12 // Remove old requests outside window 13 const recentRequests = requests.filter(time => now - time < windowMs) 14 15 if (recentRequests.length >= maxRequests) { 16 return false // Rate limit exceeded 17 } 18 19 // Add current request 20 recentRequests.push(now) 21 this.requests.set(identifier, recentRequests) 22 23 return true 24 } 25} 26 27const limiter = new RateLimiter() 28 29export async function GET(request: Request) { 30 const ip = request.headers.get('x-forwarded-for') || 'unknown' 31 32 const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min 33 34 if (!allowed) { 35 return NextResponse.json({ 36 error: 'Rate limit exceeded' 37 }, { status: 429 }) 38 } 39 40 // Continue with request 41}
Background Jobs & Queues
Simple Queue Pattern
typescript1class JobQueue<T> { 2 private queue: T[] = [] 3 private processing = false 4 5 async add(job: T): Promise<void> { 6 this.queue.push(job) 7 8 if (!this.processing) { 9 this.process() 10 } 11 } 12 13 private async process(): Promise<void> { 14 this.processing = true 15 16 while (this.queue.length > 0) { 17 const job = this.queue.shift()! 18 19 try { 20 await this.execute(job) 21 } catch (error) { 22 console.error('Job failed:', error) 23 } 24 } 25 26 this.processing = false 27 } 28 29 private async execute(job: T): Promise<void> { 30 // Job execution logic 31 } 32} 33 34// Usage for indexing markets 35interface IndexJob { 36 marketId: string 37} 38 39const indexQueue = new JobQueue<IndexJob>() 40 41export async function POST(request: Request) { 42 const { marketId } = await request.json() 43 44 // Add to queue instead of blocking 45 await indexQueue.add({ marketId }) 46 47 return NextResponse.json({ success: true, message: 'Job queued' }) 48}
Logging & Monitoring
Structured Logging
typescript1interface LogContext { 2 userId?: string 3 requestId?: string 4 method?: string 5 path?: string 6 [key: string]: unknown 7} 8 9class Logger { 10 log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) { 11 const entry = { 12 timestamp: new Date().toISOString(), 13 level, 14 message, 15 ...context 16 } 17 18 console.log(JSON.stringify(entry)) 19 } 20 21 info(message: string, context?: LogContext) { 22 this.log('info', message, context) 23 } 24 25 warn(message: string, context?: LogContext) { 26 this.log('warn', message, context) 27 } 28 29 error(message: string, error: Error, context?: LogContext) { 30 this.log('error', message, { 31 ...context, 32 error: error.message, 33 stack: error.stack 34 }) 35 } 36} 37 38const logger = new Logger() 39 40// Usage 41export async function GET(request: Request) { 42 const requestId = crypto.randomUUID() 43 44 logger.info('Fetching markets', { 45 requestId, 46 method: 'GET', 47 path: '/api/markets' 48 }) 49 50 try { 51 const markets = await fetchMarkets() 52 return NextResponse.json({ success: true, data: markets }) 53 } catch (error) { 54 logger.error('Failed to fetch markets', error as Error, { requestId }) 55 return NextResponse.json({ error: 'Internal error' }, { status: 500 }) 56 } 57}
Remember: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level.