Maximize API speed and efficiency with caching, pagination, rate limiting, and query optimization techniques.
Minimize latency between request and response for better user experience.
Number of requests your API can handle per second.
Keep response sizes minimal to reduce transfer time and bandwidth.
Optimize CPU, memory, and network usage for cost efficiency.
// Cache for 1 hour (public resources) Cache-Control: public, max-age=3600 // Cache for 5 minutes (private user data) Cache-Control: private, max-age=300 // No caching (sensitive data) Cache-Control: no-cache, no-store, must-revalidate // Cache but validate with server (ETags) Cache-Control: no-cache ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// Server response with ETag
HTTP/1.1 200 OK
ETag: "686897696a7c876b7e"
Content-Type: application/json
{ "id": 123, "name": "John Doe" }
// Client subsequent request
GET /users/123
If-None-Match: "686897696a7c876b7e"
// Server response if not modified
HTTP/1.1 304 Not Modified
ETag: "686897696a7c876b7e"
// No response body sent (saves bandwidth)
// Implementation example
function getUserWithETag(userId) {
const user = db.users.findById(userId);
const etag = generateETag(user);
if (req.headers['if-none-match'] === etag) {
return res.status(304).send();
}
return res
.set('ETag', etag)
.set('Cache-Control', 'no-cache')
.json(user);
}
function generateETag(data) {
return crypto
.createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
}// Server response
HTTP/1.1 200 OK
Last-Modified: Wed, 15 Jan 2025 10:30:00 GMT
Content-Type: application/json
{ "id": 123, "name": "John Doe" }
// Client subsequent request
GET /users/123
If-Modified-Since: Wed, 15 Jan 2025 10:30:00 GMT
// Server response if not modified
HTTP/1.1 304 Not Modified// Redis cache implementation
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost',
port: 6379
});
async function getUsers() {
const cacheKey = 'users:all';
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - fetch from database
const users = await db.users.findAll();
// Store in cache for 5 minutes
await redis.setex(
cacheKey,
300, // TTL in seconds
JSON.stringify(users)
);
return users;
}
// Cache invalidation on update
async function updateUser(userId, updates) {
const user = await db.users.update(userId, updates);
// Invalidate related cache keys
await redis.del('users:all');
await redis.del(`users:${userId}`);
return user;
}// Cache expensive queries
async function getProductsByCategory(category, filters) {
const cacheKey = `products:${category}:${JSON.stringify(filters)}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Expensive database query
const products = await db.products
.where({ category })
.filter(filters)
.sort('-created_at')
.limit(100);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(products));
return products;
}// Request
GET /users?page=2&per_page=20
// Response
{
"data": [...], // 20 users
"meta": {
"page": 2,
"per_page": 20,
"total": 156,
"total_pages": 8
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=1&per_page=20",
"next": "/users?page=3&per_page=20",
"last": "/users?page=8&per_page=20"
}
}
// Implementation
async function getUsers(page = 1, perPage = 20) {
const offset = (page - 1) * perPage;
const [users, total] = await Promise.all([
db.users.limit(perPage).offset(offset),
db.users.count()
]);
return {
data: users,
meta: {
page,
per_page: perPage,
total,
total_pages: Math.ceil(total / perPage)
}
};
}// Request (first page)
GET /users?limit=20
// Response
{
"data": [...], // 20 users
"pagination": {
"next_cursor": "eyJpZCI6MjB9", // Base64 encoded
"has_more": true
}
}
// Request (next page)
GET /users?limit=20&cursor=eyJpZCI6MjB9
// Implementation
async function getUsers(cursor = null, limit = 20) {
let query = db.users.orderBy('id');
if (cursor) {
const decoded = JSON.parse(atob(cursor));
query = query.where('id', '>', decoded.id);
}
const users = await query.limit(limit + 1);
const hasMore = users.length > limit;
if (hasMore) {
users.pop(); // Remove extra item
}
const nextCursor = hasMore
? btoa(JSON.stringify({ id: users[users.length - 1].id }))
: null;
return {
data: users,
pagination: {
next_cursor: nextCursor,
has_more: hasMore
}
};
}// Request
GET /posts?since=2025-01-15T10:30:00Z&limit=20
// Response
{
"data": [...], // 20 posts created after timestamp
"pagination": {
"oldest": "2025-01-15T08:15:00Z",
"newest": "2025-01-15T10:25:00Z"
}
}
// Implementation
async function getPosts(since = null, limit = 20) {
let query = db.posts.orderBy('created_at', 'desc');
if (since) {
query = query.where('created_at', '<', since);
}
const posts = await query.limit(limit);
return {
data: posts,
pagination: {
oldest: posts[posts.length - 1]?.created_at,
newest: posts[0]?.created_at
}
};
}// Rate limiter using Redis
class RateLimiter {
constructor(redis) {
this.redis = redis;
}
async checkLimit(userId, limit = 100, window = 900) {
// limit: max requests, window: time in seconds (900s = 15min)
const key = `rate_limit:${userId}`;
const now = Date.now();
// Remove old requests outside window
await this.redis.zremrangebyscore(key, 0, now - (window * 1000));
// Count requests in current window
const requestCount = await this.redis.zcard(key);
if (requestCount >= limit) {
// Rate limit exceeded
const oldest = await this.redis.zrange(key, 0, 0);
const resetTime = parseInt(oldest[0]) + (window * 1000);
return {
allowed: false,
limit,
remaining: 0,
resetAt: new Date(resetTime)
};
}
// Add current request
await this.redis.zadd(key, now, now);
await this.redis.expire(key, window);
return {
allowed: true,
limit,
remaining: limit - requestCount - 1,
resetAt: new Date(now + (window * 1000))
};
}
}
// Middleware usage
app.use(async (req, res, next) => {
const userId = req.user?.id || req.ip;
const result = await rateLimiter.checkLimit(userId);
// Set rate limit headers
res.set({
'X-RateLimit-Limit': result.limit,
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': result.resetAt.toISOString()
});
if (!result.allowed) {
return res.status(429).json({
error: 'Too many requests',
retry_after: Math.ceil((result.resetAt - Date.now()) / 1000)
});
}
next();
});// Example: per-route limits in your own API
const RATE_LIMITS = {
read: { limit: 300, window: 60 }, // 300 req/min
write: { limit: 60, window: 60 }, // 60 req/min
auth: { limit: 5, window: 900 }, // 5 req/15min
};
function getRateLimit(method) {
if (method === 'GET') return RATE_LIMITS.read;
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
return RATE_LIMITS.write;
}
return RATE_LIMITS.auth;
}// Respect rate limits in API client
async function fetchWithRateLimit(url, options = {}) {
const response = await fetch(url, options);
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`Rate limit: ${remaining}/${limit} remaining`);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.warn(`Rate limited. Retry after ${retryAfter} seconds`);
// Wait and retry
await new Promise(resolve =>
setTimeout(resolve, retryAfter * 1000)
);
return fetchWithRateLimit(url, options);
}
// Warn when approaching limit
if (remaining < limit * 0.1) {
console.warn(`Approaching rate limit: ${remaining} remaining`);
}
return response;
}Only return requested fields to reduce payload size:
// Request specific fields
GET /users?fields=id,name,email
// Response (only requested fields)
{
"data": [
{ "id": 1, "name": "John", "email": "john@example.com" },
{ "id": 2, "name": "Jane", "email": "jane@example.com" }
]
}
// Implementation
async function getUsers(fields = null) {
let query = db.users;
if (fields) {
const fieldArray = fields.split(',').map(f => f.trim());
query = query.select(fieldArray);
}
return await query;
}
// Savings example:
// Full user object: ~2KB
// Selected fields: ~200 bytes
// 90% reduction in payload size!// ❌ BAD: N+1 queries
async function getUsersWithPosts() {
const users = await db.users.findAll(); // 1 query
for (const user of users) {
user.posts = await db.posts.where({ user_id: user.id }); // N queries
}
return users;
}
// Total: 1 + N queries (if 100 users = 101 queries!)
// ✅ GOOD: Single query with join
async function getUsersWithPosts() {
return await db.users
.include('posts') // Single JOIN query
.findAll();
}
// Total: 1 query
// Alternative: Batch loading
async function getUsersWithPosts() {
const users = await db.users.findAll();
const userIds = users.map(u => u.id);
const posts = await db.posts
.whereIn('user_id', userIds)
.findAll();
// Group posts by user
const postsByUser = posts.reduce((acc, post) => {
if (!acc[post.user_id]) acc[post.user_id] = [];
acc[post.user_id].push(post);
return acc;
}, {});
users.forEach(user => {
user.posts = postsByUser[user.id] || [];
});
return users;
}
// Total: 2 queries (much better!)// Create indexes on frequently queried fields CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_posts_user_id ON posts(user_id); CREATE INDEX idx_posts_created_at ON posts(created_at); // Composite index for multiple filters CREATE INDEX idx_products_category_price ON products(category, price); // Before indexing: 2500ms query time // After indexing: 15ms query time // 166x faster!
// Database connection pool
const pool = new Pool({
host: 'localhost',
database: 'myapp',
max: 20, // Maximum connections
min: 5, // Minimum connections
idle: 10000 // Close idle connections after 10s
});
// Reuse connections instead of creating new ones
async function query(sql, params) {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release(); // Return to pool
}
}Compress responses to reduce bandwidth usage by 70-90%:
import compression from 'compression';
import express from 'express';
import { CodeBlock } from '@/app/components/CodeBlock';
const app = express();
// Enable GZIP compression
app.use(compression({
level: 6, // Compression level (0-9)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Don't compress if client doesn't support it
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));| Content Type | Original | Compressed | Savings |
|---|---|---|---|
| JSON (API response) | 100 KB | 15 KB | 85% |
| HTML | 80 KB | 20 KB | 75% |
| CSS/JavaScript | 200 KB | 50 KB | 75% |