| name | backend-patterns |
| description | Use when building backend services with Node.js, Express, or Next.js API routes. Covers architecture patterns, database optimization, repository/service layers, and server-side best practices. |
| origin | MCC |
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
GET /api/markets # List resources
GET /api/markets/:id # Get single resource
POST /api/markets # Create resource
PUT /api/markets/:id # Replace resource
PATCH /api/markets/:id # Update resource
DELETE /api/markets/:id # Delete resource
GET /api/markets?status=active&sort=volume&limit=20&offset=0
Repository Pattern
interface MarketRepository {
findAll(filters?: MarketFilters): Promise<Market[]>
findById(id: string): Promise<Market | null>
create(data: CreateMarketDto): Promise<Market>
update(id: string, data: UpdateMarketDto): Promise<Market>
delete(id: string): Promise<void>
}
Abstract data access behind a consistent interface. Concrete implementations handle storage details (database, API, file). Business logic depends on the interface, not the storage mechanism.
Service Layer Pattern
Separate business logic from data access:
class MarketService {
constructor(private marketRepo: MarketRepository) {}
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
const embedding = await generateEmbedding(query)
const results = await this.vectorSearch(embedding, limit)
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
return markets.sort((a, b) => {
const scoreA = results.find(r => r.id === a.id)?.score || 0
const scoreB = results.find(r => r.id === b.id)?.score || 0
return scoreA - scoreB
})
}
}
Middleware Pattern
export function withAuth(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'Unauthorized' })
try {
req.user = await verifyToken(token)
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
}
Database Patterns
N+1 Query Prevention
const markets = await getMarkets()
for (const market of markets) {
market.creator = await getUser(market.creator_id)
}
const markets = await getMarkets()
const creatorIds = markets.map(m => m.creator_id)
const creators = await getUsers(creatorIds)
const creatorMap = new Map(creators.map(c => [c.id, c]))
markets.forEach(market => {
market.creator = creatorMap.get(market.creator_id)
})
Query Optimization
- Select only needed columns, not
SELECT *
- Use indexes on frequently filtered/sorted columns
- Use
LIMIT on all queries
- Use JOINs or batch fetches instead of N+1
Error Handling
Centralized Error Handler
class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message)
}
}
export function errorHandler(error: unknown, req: Request): Response {
if (error instanceof ApiError) {
return NextResponse.json({ success: false, error: error.message }, { status: error.statusCode })
}
if (error instanceof z.ZodError) {
return NextResponse.json({ success: false, error: 'Validation failed', details: error.errors }, { status: 400 })
}
console.error('Unexpected error:', error)
.({ : , : }, { : })
}
Retry with Exponential Backoff
async function fetchWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000))
}
}
}
throw lastError!
}
Caching Strategies
Cache-Aside Pattern
- Check cache first
- On miss, fetch from database
- Store result in cache with TTL
- Invalidate cache on writes
class CachedMarketRepository implements MarketRepository {
constructor(private baseRepo: MarketRepository, private redis: RedisClient) {}
async findById(id: string): Promise<Market | null> {
const cached = await this.redis.get(`market:${id}`)
if (cached) return JSON.parse(cached)
const market = await this.baseRepo.findById(id)
if (market) await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
return market
}
}
Quick Reference
| Pattern | When to Use |
|---|
| Repository | Abstract data access, enable testing with mocks |
| Service Layer | Separate business logic from data access |
| Middleware | Cross-cutting concerns (auth, logging, rate limiting) |
| Cache-Aside | Reduce database load for read-heavy data |
| Retry + Backoff | Resilience against transient failures |
| Job Queue | Defer expensive work, avoid blocking requests |
Reference Files
Remember: Choose patterns that fit your complexity level. Start simple, add layers when needed.