Building Scalable APIs with Node.js
Building APIs that can handle millions of requests requires careful consideration of architecture, patterns, and best practices. In this note, I'll share some insights from my experience building APIs at scale.
Project Structure
A well-organized project structure is the foundation of maintainable code:
src/
├── controllers/ # Request handlers
├── services/ # Business logic
├── repositories/ # Data access layer
├── middleware/ # Express middleware
├── utils/ # Helper functions
├── types/ # TypeScript types
└── routes/ # Route definitions
Key Principles
1. Separation of Concerns
Keep your controllers thin. They should only handle HTTP concerns:
// controllers/user.controller.ts
export class UserController {
constructor(private userService: UserService) {}
async getUser(req: Request, res: Response) {
const { id } = req.params
const user = await this.userService.findById(id)
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
return res.json(user)
}
}
2. Error Handling
Centralize your error handling with custom error classes:
// utils/errors.ts
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public code?: string
) {
super(message)
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND')
}
}
3. Validation
Always validate input data. I recommend using Zod for runtime validation:
import { z } from 'zod'
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().positive().optional(),
})
type CreateUserInput = z.infer<typeof createUserSchema>
Performance Tips
- Use connection pooling for database connections
- Implement caching with Redis for frequently accessed data
- Add rate limiting to prevent abuse
- Use compression for response payloads
- Implement proper logging with structured logs
Conclusion
Building scalable APIs is about making the right architectural decisions early and maintaining code quality throughout the project lifecycle. Start simple, measure performance, and optimize based on real data.