Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes.
Backend Development Patterns
Backend architecture patterns and best practices for scalable server-side applications.
API Design Patterns
RESTful API Structure
1// ✅ 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=0Repository Pattern
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
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
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
1// ✅ 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.from("markets").select("*");N+1 Query Prevention
1// ❌ 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
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
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
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
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 {
16 success: false,
17 error: error.message,
18 },
19 { status: error.statusCode },
20 );
21 }
22
23 if (error instanceof z.ZodError) {
24 return NextResponse.json(
25 {
26 success: false,
27 error: "Validation failed",
28 details: error.errors,
29 },
30 { status: 400 },
31 );
32 }
33
34 // Log unexpected errors
35 console.error("Unexpected error:", error);
36
37 return NextResponse.json(
38 {
39 success: false,
40 error: "Internal server error",
41 },
42 { status: 500 },
43 );
44}
45
46// Usage
47export async function GET(request: Request) {
48 try {
49 const data = await fetchData();
50 return NextResponse.json({ success: true, data });
51 } catch (error) {
52 return errorHandler(error, request);
53 }
54}Retry with Exponential Backoff
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
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
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")(async (
34 request: Request,
35 user: User,
36) => {
37 // Handler receives authenticated user with verified permission
38 return new Response("Deleted", { status: 200 });
39});Rate Limiting
Simple In-Memory Rate Limiter
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 {
37 error: "Rate limit exceeded",
38 },
39 { status: 429 },
40 );
41 }
42
43 // Continue with request
44}Background Jobs & Queues
Simple Queue Pattern
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
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.