Tutorials

API Development Best Practices: Complete Guide for 2025

API Development Best Practices: Complete Guide for 2025 - Featured image for CodeWise AI blog post

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

  1. API Design Fundamentals
  2. REST API Best Practices
  3. GraphQL Best Practices
  4. Authentication & Authorization
  5. Rate Limiting & Throttling
  6. API Versioning Strategies
  7. Error Handling
  8. Request Validation
  9. Response Design
  10. API Documentation
  11. Testing Strategies
  12. Performance Optimization
  13. Security Hardening
  14. Monitoring & Analytics
  15. 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

javascript
// Bad: Implementation-focused
GET /api/getUserDataFromDatabaseById?userId=123

// Good: User-focused
GET /api/users/123

2. Use Nouns, Not Verbs

javascript
// 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

javascript
// 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

text
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:

PatternBest ForProsCons
RESTCRUD operations, public APIsSimple, cacheable, well-understoodOver-fetching, multiple requests
GraphQLComplex data requirements, mobile appsFlexible, single request, type-safeComplex caching, learning curve
gRPCMicroservices, high-performanceFast, bi-directional streamingNot browser-friendly, HTTP/2 required
WebSocketReal-time, bidirectionalLow latency, persistent connectionComplex scaling, stateful
Server-Sent EventsServer → Client updatesSimple, auto-reconnectUnidirectional only

REST API Best Practices

1. Resource Naming Conventions

Use Plural Nouns:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
{
  "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:

graphql
# 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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)

javascript
// 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

javascript
// 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

javascript
// 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

javascript
// 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

javascript
// 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

javascript
// 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

javascript
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

javascript
// 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

javascript
// 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

  1. Start with basics: Build simple CRUD API
  2. Add security: Implement authentication
  3. Optimize: Add caching and rate limiting
  4. Document: Write comprehensive API docs
  5. Test: Achieve 80%+ code coverage
  6. Monitor: Set up logging and analytics
  7. 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

Tags:APIRESTGraphQLBackendBest PracticesTutorialAuthenticationNode.js

Share this article

Related Articles