API Development Best Practices: Complete Guide for 2025

Master API development with this comprehensive 2025 guide. Learn REST, GraphQL, authentication, rate limiting, versioning, testing, and documentation. Includes real-world examples and code samples.
API Development Best Practices: Complete Guide for 2025
Last Updated: November 26, 2025 | Reading Time: 18 minutes | Skill Level: Intermediate to Advanced
Building APIs that are secure, scalable, and maintainable is one of the most critical skills for backend developers in 2025. Poor API design can lead to security vulnerabilities, performance issues, and frustrated developers trying to integrate with your service.
This comprehensive guide covers everything you need to build production-ready APIs, including:
- ✅ REST vs GraphQL: When to use each
- ✅ Authentication & Authorization best practices
- ✅ Rate limiting and throttling strategies
- ✅ API versioning approaches
- ✅ Error handling and status codes
- ✅ Documentation and testing
- ✅ Performance optimization
- ✅ Security hardening
- ✅ Real-world examples in Node.js, Python, and Go
What You'll Build: By the end, you'll know how to build enterprise-grade APIs that handle millions of requests.
📊 Table of Contents
- API Design Fundamentals
- REST API Best Practices
- GraphQL Best Practices
- Authentication & Authorization
- Rate Limiting & Throttling
- API Versioning Strategies
- Error Handling
- Request Validation
- Response Design
- API Documentation
- Testing Strategies
- Performance Optimization
- Security Hardening
- Monitoring & Analytics
- Real-World Examples
API Design Fundamentals
The API Design Philosophy
Great APIs are:
- Intuitive: Developers can predict endpoint behavior
- Consistent: Similar patterns across all endpoints
- Well-documented: Clear examples and explanations
- Backward-compatible: Changes don't break existing clients
- Secure: Protected against common vulnerabilities
- Performant: Fast response times under load
Key Principles
1. Design for Your Users
// Bad: Implementation-focused
GET /api/getUserDataFromDatabaseById?userId=123
// Good: User-focused
GET /api/users/123
2. Use Nouns, Not Verbs
// Bad
POST / api / createUser
GET / api / getUsers
DELETE / api / deleteUser / 123
// Good
POST / api / users
GET / api / users
DELETE / api / users / 123
3. Be Consistent
// Bad: Mixed conventions
GET / api / Users // Capital U
GET / api / user - profiles // Kebab case
GET / api / UserOrders // Mixed case
// Good: Consistent convention
GET / api / users
GET / api / user - profiles
GET / api / user - orders
4. Use HTTP Methods Correctly
GET - Retrieve resources (safe, idempotent)
POST - Create new resources
PUT - Replace entire resource (idempotent)
PATCH - Partial update (idempotent)
DELETE - Remove resource (idempotent)
API Architecture Patterns
Pattern Comparison Table:
| Pattern | Best For | Pros | Cons |
|---|---|---|---|
| REST | CRUD operations, public APIs | Simple, cacheable, well-understood | Over-fetching, multiple requests |
| GraphQL | Complex data requirements, mobile apps | Flexible, single request, type-safe | Complex caching, learning curve |
| gRPC | Microservices, high-performance | Fast, bi-directional streaming | Not browser-friendly, HTTP/2 required |
| WebSocket | Real-time, bidirectional | Low latency, persistent connection | Complex scaling, stateful |
| Server-Sent Events | Server → Client updates | Simple, auto-reconnect | Unidirectional only |
REST API Best Practices
1. Resource Naming Conventions
Use Plural Nouns:
// Good
GET / api / users // Get all users
POST / api / users // Create user
GET / api / users / 123 // Get specific user
PUT / api / users / 123 // Update user
DELETE / api / users / 123 // Delete user
Nested Resources:
// User's posts
GET / api / users / 123 / posts
POST / api / users / 123 / posts
GET / api / users / 123 / posts / 456
// User's comments on a post
GET / api / users / 123 / posts / 456 / comments
// Limit nesting to 2-3 levels maximum
Collections and Filters:
// Pagination
GET /api/users?page=2&limit=20
// Filtering
GET /api/users?role=admin&status=active
// Sorting
GET /api/users?sort=createdAt:desc
// Field selection
GET /api/users?fields=id,name,email
// Searching
GET /api/users?search=john
// Combined
GET /api/users?role=admin&page=1&limit=10&sort=name:asc
2. HTTP Status Codes
Use Appropriate Status Codes:
// Success Codes
200 OK - Successful GET, PUT, PATCH, or DELETE
201 Created - Successful POST that creates a resource
204 No Content - Successful DELETE with no response body
// Client Error Codes
400 Bad Request - Invalid request syntax or validation error
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Request conflicts with current state
422 Unprocessable - Validation error with details
429 Too Many - Rate limit exceeded
// Server Error Codes
500 Internal Error - Unexpected server error
502 Bad Gateway - Invalid response from upstream
503 Service Unavail - Server temporarily unavailable
504 Gateway Timeout - Upstream server timeout
Example Implementation:
// Express.js example
const express = require("express")
const app = express()
app.post("/api/users", async (req, res) => {
try {
// Validate input
const errors = validateUser(req.body)
if (errors.length > 0) {
return res.status(422).json({
success: false,
message: "Validation failed",
errors: errors,
})
}
// Check if user exists
const existingUser = await User.findByEmail(req.body.email)
if (existingUser) {
return res.status(409).json({
success: false,
message: "User with this email already exists",
})
}
// Create user
const user = await User.create(req.body)
res.status(201).json({
success: true,
message: "User created successfully",
data: {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
},
})
} catch (error) {
console.error("User creation error:", error)
res.status(500).json({
success: false,
message: "Internal server error",
error: process.env.NODE_ENV === "development" ? error.message : undefined,
})
}
})
3. Request/Response Structure
Consistent Response Format:
// Success response
{
"success": true,
"message": "Users retrieved successfully",
"data": {
"users": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
},
"meta": {
"timestamp": "2025-11-26T10:30:00Z",
"version": "1.0",
"requestId": "abc-123-def"
}
}
// Error response
{
"success": false,
"message": "Validation failed",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_EMAIL"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"code": "PASSWORD_TOO_SHORT"
}
],
"meta": {
"timestamp": "2025-11-26T10:30:00Z",
"requestId": "abc-123-def"
}
}
4. HATEOAS (Hypermedia)
Include Links to Related Resources:
{
"success": true,
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"links": {
"self": "/api/users/123",
"posts": "/api/users/123/posts",
"comments": "/api/users/123/comments",
"avatar": "/api/users/123/avatar"
}
}
}
GraphQL Best Practices
1. Schema Design
Well-Structured Schema:
# types.graphql
type User {
id: ID!
name: String!
email: String!
avatar: String
posts(first: Int, after: String): PostConnection!
followers(first: Int): [User!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments(first: Int, after: String): CommentConnection!
likes: Int!
published: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
}
# Pagination using Relay Cursor Connections
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Queries
type Query {
user(id: ID!): User
users(first: Int, after: String, filter: UserFilter): UserConnection!
post(id: ID!): Post
posts(first: Int, after: String, filter: PostFilter): PostConnection!
searchPosts(query: String!, first: Int): [Post!]!
}
# Mutations
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
createPost(input: CreatePostInput!): CreatePostPayload!
publishPost(id: ID!): PublishPostPayload!
likePost(id: ID!): LikePostPayload!
}
# Input types
input CreateUserInput {
name: String!
email: String!
password: String!
avatar: String
}
input UpdateUserInput {
name: String
email: String
avatar: String
}
input UserFilter {
role: Role
status: UserStatus
createdAfter: DateTime
}
# Enums
enum Role {
ADMIN
USER
MODERATOR
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
}
# Scalars
scalar DateTime
scalar Email
scalar URL
2. Resolver Best Practices
Efficient Resolvers:
// resolvers.js
const resolvers = {
Query: {
user: async (_, { id }, { dataSources, user }) => {
// Check authentication
if (!user) {
throw new AuthenticationError("Must be logged in")
}
return dataSources.userAPI.getUserById(id)
},
users: async (_, { first, after, filter }, { dataSources }) => {
return dataSources.userAPI.getUsers({ first, after, filter })
},
},
Mutation: {
createPost: async (_, { input }, { dataSources, user }) => {
if (!user) {
throw new AuthenticationError("Must be logged in")
}
// Validate input
const errors = validatePostInput(input)
if (errors.length > 0) {
throw new UserInputError("Validation failed", { errors })
}
const post = await dataSources.postAPI.createPost({
...input,
authorId: user.id,
})
return {
success: true,
message: "Post created successfully",
post,
}
},
},
User: {
// Solve N+1 problem with DataLoader
posts: async (user, { first, after }, { dataSources }) => {
return dataSources.postAPI.getPostsByUserId(user.id, { first, after })
},
followers: async (user, { first }, { loaders }) => {
// Use DataLoader for batching
return loaders.followerLoader.load(user.id)
},
},
Post: {
author: async (post, _, { loaders }) => {
// Batch load authors
return loaders.userLoader.load(post.authorId)
},
comments: async (post, { first, after }, { dataSources }) => {
return dataSources.commentAPI.getCommentsByPostId(post.id, { first, after })
},
},
}
3. N+1 Problem Solution with DataLoader
Implement DataLoader:
// dataloaders.js
const DataLoader = require("dataloader")
const createLoaders = (dataSources) => ({
userLoader: new DataLoader(async (userIds) => {
const users = await dataSources.userAPI.getUsersByIds(userIds)
// Return users in same order as userIds
return userIds.map((id) => users.find((user) => user.id === id))
}),
postLoader: new DataLoader(async (postIds) => {
const posts = await dataSources.postAPI.getPostsByIds(postIds)
return postIds.map((id) => posts.find((post) => post.id === id))
}),
followerLoader: new DataLoader(async (userIds) => {
const followersMap = await dataSources.followAPI.getFollowersByUserIds(userIds)
return userIds.map((id) => followersMap[id] || [])
}),
})
module.exports = createLoaders
4. Error Handling in GraphQL
Custom Error Classes:
// errors.js
const { ApolloError } = require("apollo-server-express")
class AuthenticationError extends ApolloError {
constructor(message) {
super(message, "UNAUTHENTICATED")
}
}
class AuthorizationError extends ApolloError {
constructor(message) {
super(message, "FORBIDDEN")
}
}
class ValidationError extends ApolloError {
constructor(message, errors) {
super(message, "VALIDATION_ERROR")
this.errors = errors
}
}
class NotFoundError extends ApolloError {
constructor(resource) {
super(`${resource} not found`, "NOT_FOUND")
}
}
module.exports = {
AuthenticationError,
AuthorizationError,
ValidationError,
NotFoundError,
}
Authentication & Authorization
1. JWT (JSON Web Tokens)
Complete JWT Implementation:
// auth/jwt.js
const jwt = require("jsonwebtoken")
const crypto = require("crypto")
class JWTService {
constructor() {
this.accessTokenSecret = process.env.ACCESS_TOKEN_SECRET
this.refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET
this.accessTokenExpiry = "15m"
this.refreshTokenExpiry = "7d"
}
generateTokens(userId, payload = {}) {
const accessToken = jwt.sign({ userId, ...payload }, this.accessTokenSecret, { expiresIn: this.accessTokenExpiry })
const refreshToken = jwt.sign({ userId, tokenId: crypto.randomUUID() }, this.refreshTokenSecret, {
expiresIn: this.refreshTokenExpiry,
})
return { accessToken, refreshToken }
}
verifyAccessToken(token) {
try {
return jwt.verify(token, this.accessTokenSecret)
} catch (error) {
if (error.name === "TokenExpiredError") {
throw new Error("Access token expired")
}
throw new Error("Invalid access token")
}
}
verifyRefreshToken(token) {
try {
return jwt.verify(token, this.refreshTokenSecret)
} catch (error) {
throw new Error("Invalid refresh token")
}
}
refreshAccessToken(refreshToken) {
const decoded = this.verifyRefreshToken(refreshToken)
// Verify refresh token hasn't been revoked
// (check against Redis/database)
const { accessToken } = this.generateTokens(decoded.userId)
return accessToken
}
}
module.exports = new JWTService()
Authentication Middleware:
// middleware/auth.js
const jwtService = require("../auth/jwt")
const User = require("../models/User")
const authenticate = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({
success: false,
message: "No token provided",
})
}
const token = authHeader.substring(7)
// Verify token
const decoded = jwtService.verifyAccessToken(token)
// Get user from database
const user = await User.findById(decoded.userId).select("-password")
if (!user) {
return res.status(401).json({
success: false,
message: "User not found",
})
}
// Check if user is active
if (user.status !== "active") {
return res.status(403).json({
success: false,
message: "Account is not active",
})
}
// Attach user to request
req.user = user
next()
} catch (error) {
res.status(401).json({
success: false,
message: error.message,
})
}
}
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: "Authentication required",
})
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: "Insufficient permissions",
})
}
next()
}
}
module.exports = { authenticate, authorize }
2. API Keys
API Key Management:
// auth/apiKey.js
const crypto = require("crypto")
const ApiKey = require("../models/ApiKey")
class ApiKeyService {
generateApiKey() {
// Generate cryptographically secure random key
return `sk_${crypto.randomBytes(32).toString("hex")}`
}
async createApiKey(userId, name, permissions = []) {
const key = this.generateApiKey()
const hashedKey = this.hashApiKey(key)
const apiKey = await ApiKey.create({
userId,
name,
keyHash: hashedKey,
permissions,
lastUsedAt: null,
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
})
// Return plain key only once
return { key, apiKey }
}
hashApiKey(key) {
return crypto.createHash("sha256").update(key).digest("hex")
}
async verifyApiKey(key) {
const hashedKey = this.hashApiKey(key)
const apiKey = await ApiKey.findOne({
keyHash: hashedKey,
isActive: true,
}).populate("user")
if (!apiKey) {
throw new Error("Invalid API key")
}
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
throw new Error("API key expired")
}
// Update last used timestamp
apiKey.lastUsedAt = new Date()
apiKey.requestCount += 1
await apiKey.save()
return apiKey
}
}
module.exports = new ApiKeyService()
3. OAuth 2.0
OAuth Flow Implementation:
// auth/oauth.js
const axios = require("axios")
const jwt = require("jsonwebtoken")
class OAuthService {
async getGoogleAuthUrl() {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
response_type: "code",
scope: "openid email profile",
access_type: "offline",
prompt: "consent",
})
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`
}
async handleGoogleCallback(code) {
// Exchange code for tokens
const tokenResponse = await axios.post("https://oauth2.googleapis.com/token", {
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: "authorization_code",
})
const { access_token, id_token, refresh_token } = tokenResponse.data
// Decode ID token to get user info
const userInfo = jwt.decode(id_token)
// Find or create user
let user = await User.findOne({ googleId: userInfo.sub })
if (!user) {
user = await User.create({
googleId: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
avatar: userInfo.picture,
provider: "google",
refreshToken: refresh_token,
})
} else {
user.refreshToken = refresh_token
await user.save()
}
// Generate our JWT tokens
const tokens = jwtService.generateTokens(user.id)
return { user, tokens }
}
}
module.exports = new OAuthService()
Rate Limiting & Throttling
1. Token Bucket Algorithm
Redis-based Rate Limiter:
// middleware/rateLimiter.js
const Redis = require("ioredis")
const redis = new Redis(process.env.REDIS_URL)
class RateLimiter {
constructor(options = {}) {
this.windowMs = options.windowMs || 60000 // 1 minute
this.maxRequests = options.maxRequests || 100
this.keyPrefix = options.keyPrefix || "rl:"
}
async consume(identifier) {
const key = `${this.keyPrefix}${identifier}`
const now = Date.now()
const windowStart = now - this.windowMs
// Use Redis sorted set for sliding window
const multi = redis.multi()
// Remove old entries
multi.zremrangebyscore(key, 0, windowStart)
// Count requests in current window
multi.zcard(key)
// Add current request
multi.zadd(key, now, `${now}-${Math.random()}`)
// Set expiry
multi.expire(key, Math.ceil(this.windowMs / 1000))
const results = await multi.exec()
const requestCount = results[1][1]
return {
allowed: requestCount < this.maxRequests,
remaining: Math.max(0, this.maxRequests - requestCount - 1),
resetAt: now + this.windowMs,
}
}
middleware() {
return async (req, res, next) => {
// Use API key, user ID, or IP address as identifier
const identifier = req.user?.id || req.headers["x-api-key"] || req.ip
try {
const result = await this.consume(identifier)
// Set rate limit headers
res.setHeader("X-RateLimit-Limit", this.maxRequests)
res.setHeader("X-RateLimit-Remaining", result.remaining)
res.setHeader("X-RateLimit-Reset", Math.ceil(result.resetAt / 1000))
if (!result.allowed) {
return res.status(429).json({
success: false,
message: "Too many requests",
retryAfter: Math.ceil((result.resetAt - Date.now()) / 1000),
})
}
next()
} catch (error) {
console.error("Rate limiter error:", error)
next() // Fail open
}
}
}
}
// Usage
const apiLimiter = new RateLimiter({
windowMs: 60000, // 1 minute
maxRequests: 100, // 100 requests per minute
})
const strictLimiter = new RateLimiter({
windowMs: 60000,
maxRequests: 10, // 10 requests per minute for sensitive endpoints
})
module.exports = { apiLimiter, strictLimiter }
2. Tiered Rate Limiting
Different Limits for Different Users:
// middleware/tieredRateLimiter.js
const RateLimiter = require("./rateLimiter")
const rateLimitTiers = {
free: { windowMs: 60000, maxRequests: 10 },
basic: { windowMs: 60000, maxRequests: 100 },
pro: { windowMs: 60000, maxRequests: 1000 },
enterprise: { windowMs: 60000, maxRequests: 10000 },
}
const tieredRateLimiter = async (req, res, next) => {
const tier = req.user?.subscription || "free"
const config = rateLimitTiers[tier]
const limiter = new RateLimiter(config)
const identifier = req.user?.id || req.ip
const result = await limiter.consume(identifier)
res.setHeader("X-RateLimit-Limit", config.maxRequests)
res.setHeader("X-RateLimit-Remaining", result.remaining)
res.setHeader("X-RateLimit-Tier", tier)
if (!result.allowed) {
return res.status(429).json({
success: false,
message: `Rate limit exceeded for ${tier} tier`,
upgradeUrl: "/pricing",
})
}
next()
}
module.exports = tieredRateLimiter
API Versioning Strategies
1. URL Versioning (Recommended)
// v1/routes/users.js
router.get("/api/v1/users", getUsers)
// v2/routes/users.js
router.get("/api/v2/users", getUsersV2)
// app.js
app.use("/api/v1", require("./v1/routes"))
app.use("/api/v2", require("./v2/routes"))
2. Header Versioning
// middleware/apiVersion.js
const apiVersion = (req, res, next) => {
const version = req.headers["api-version"] || req.headers["accept-version"] || "1.0"
req.apiVersion = version
res.setHeader("API-Version", version)
next()
}
// Route handler
router.get("/api/users", async (req, res) => {
if (req.apiVersion === "2.0") {
return getUsersV2(req, res)
}
return getUsersV1(req, res)
})
3. Deprecation Strategy
// Deprecation warning
const deprecate = (version, sunsetDate, alternativeUrl) => {
return (req, res, next) => {
res.setHeader("Deprecation", "true")
res.setHeader("Sunset", sunsetDate)
res.setHeader("Link", `<${alternativeUrl}>; rel="alternate"`)
console.warn(`Deprecated API v${version} accessed: ${req.path}`)
next()
}
}
// Usage
router.get("/api/v1/users", deprecate("1.0", "2026-12-31", "/api/v2/users"), getUsers)
Error Handling
Centralized Error Handler
// middleware/errorHandler.js
class AppError extends Error {
constructor(message, statusCode, errorCode = null) {
super(message)
this.statusCode = statusCode
this.errorCode = errorCode
this.isOperational = true
Error.captureStackTrace(this, this.constructor)
}
}
const errorHandler = (err, req, res, next) => {
let error = { ...err }
error.message = err.message
// Log error
console.error("Error:", {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
user: req.user?.id,
})
// Mongoose validation error
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => ({
field: e.path,
message: e.message,
}))
error = new AppError("Validation failed", 422, "VALIDATION_ERROR")
error.errors = errors
}
// Mongoose duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0]
error = new AppError(`${field} already exists`, 409, "DUPLICATE_KEY")
}
// JWT errors
if (err.name === "JsonWebTokenError") {
error = new AppError("Invalid token", 401, "INVALID_TOKEN")
}
if (err.name === "TokenExpiredError") {
error = new AppError("Token expired", 401, "TOKEN_EXPIRED")
}
// Send error response
res.status(error.statusCode || 500).json({
success: false,
message: error.message || "Internal server error",
errorCode: error.errorCode,
errors: error.errors,
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
meta: {
timestamp: new Date().toISOString(),
requestId: req.id,
},
})
}
module.exports = { AppError, errorHandler }
Performance Optimization
1. Database Query Optimization
// Bad: N+1 query problem
const users = await User.find()
for (const user of users) {
user.posts = await Post.find({ userId: user.id })
}
// Good: Use populate or join
const users = await User.find().populate("posts")
// Better: Use aggregation for complex queries
const users = await User.aggregate([
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "posts",
},
},
{
$addFields: {
postCount: { $size: "$posts" },
},
},
])
2. Caching Strategy
// middleware/cache.js
const Redis = require("ioredis")
const redis = new Redis(process.env.REDIS_URL)
const cache = (duration = 300) => {
return async (req, res, next) => {
if (req.method !== "GET") {
return next()
}
const key = `cache:${req.originalUrl}`
try {
const cachedData = await redis.get(key)
if (cachedData) {
res.setHeader("X-Cache", "HIT")
return res.json(JSON.parse(cachedData))
}
res.setHeader("X-Cache", "MISS")
// Store original json function
const originalJson = res.json.bind(res)
// Override json function
res.json = (data) => {
redis.setex(key, duration, JSON.stringify(data))
return originalJson(data)
}
next()
} catch (error) {
console.error("Cache error:", error)
next()
}
}
}
// Usage
router.get("/api/users", cache(300), getUsers) // Cache for 5 minutes
3. Response Compression
const compression = require("compression")
app.use(
compression({
filter: (req, res) => {
if (req.headers["x-no-compression"]) {
return false
}
return compression.filter(req, res)
},
level: 6,
})
)
Security Hardening
Complete Security Checklist
// security/middleware.js
const helmet = require("helmet")
const cors = require("cors")
const mongoSanitize = require("express-mongo-sanitize")
const xss = require("xss-clean")
const hpp = require("hpp")
const securityMiddleware = (app) => {
// Set security headers
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
})
)
// CORS configuration
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || "*",
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization", "X-API-Key"],
})
)
// Sanitize data
app.use(mongoSanitize()) // Prevent NoSQL injection
app.use(xss()) // Prevent XSS attacks
app.use(hpp()) // Prevent parameter pollution
// Request size limits
app.use(express.json({ limit: "10mb" }))
app.use(express.urlencoded({ extended: true, limit: "10mb" }))
}
module.exports = securityMiddleware
Real-World Complete API Example
// Complete production-ready API
const express = require("express")
const mongoose = require("mongoose")
const { authenticate, authorize } = require("./middleware/auth")
const { apiLimiter } = require("./middleware/rateLimiter")
const { cache } = require("./middleware/cache")
const { errorHandler } = require("./middleware/errorHandler")
const securityMiddleware = require("./security/middleware")
const app = express()
// Security
securityMiddleware(app)
// Rate limiting
app.use("/api/", apiLimiter.middleware())
// Routes
const userRoutes = require("./routes/users")
const postRoutes = require("./routes/posts")
app.use("/api/v1/users", userRoutes)
app.use("/api/v1/posts", postRoutes)
// Health check
app.get("/health", (req, res) => {
res.json({
status: "OK",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
})
})
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: "Endpoint not found",
availableEndpoints: ["GET /api/v1/users", "POST /api/v1/users", "GET /api/v1/posts"],
})
})
// Error handler
app.use(errorHandler)
// Start server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`API server running on port ${PORT}`)
})
Conclusion
Building production-ready APIs requires attention to:
✅ Consistent Design: Follow REST/GraphQL best practices ✅ Security: Authentication, authorization, input validation ✅ Performance: Caching, optimization, efficient queries ✅ Reliability: Error handling, rate limiting, monitoring ✅ Documentation: Clear, comprehensive, up-to-date ✅ Testing: Unit, integration, and load testing
Quick Reference Checklist
Before Going to Production:
- All endpoints require authentication
- Rate limiting implemented
- Input validation on all endpoints
- Error handling centralized
- API versioning strategy in place
- Comprehensive tests (80%+ coverage)
- Documentation complete
- Monitoring and logging configured
- Security headers set
- CORS configured correctly
- Database queries optimized
- Caching implemented for read-heavy endpoints
Next Steps
- Start with basics: Build simple CRUD API
- Add security: Implement authentication
- Optimize: Add caching and rate limiting
- Document: Write comprehensive API docs
- Test: Achieve 80%+ code coverage
- Monitor: Set up logging and analytics
- Iterate: Gather feedback and improve
Ready to build your next API? Use this guide as your reference and build APIs that developers love to use!
Have questions? Comment below or join our developer community!
Found this helpful? Share with fellow developers and bookmark for future reference!
Last updated: November 26, 2025 | Next update: December 2025 | Author: CodeWise AI Team