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
typescript
1// PASS: 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// PASS: Query parameters for filtering, sorting, pagination
10GET /api/markets?status=active&sort=volume&limit=20&offset=0
Repository Pattern
typescript
1// 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
typescript
1// 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
typescript
1// 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
typescript
1// PASS: 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// FAIL: BAD: Select everything
10const { data } = await supabase
11 .from('markets')
12 .select('*')
N+1 Query Prevention
typescript
1// FAIL: 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// PASS: 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
typescript
1async 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
typescript
1class 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
typescript
1async 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
typescript
1class 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
typescript
1async 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
typescript
1import 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
typescript
1type 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
typescript
1class 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
typescript
1class 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
typescript
1interface 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.