Building scalable APIs is crucial for modern applications. This guide covers best practices for creating robust, maintainable APIs using Node.js and TypeScript.
Project Setup
Initialize Your Project
Start with a solid foundation:
mkdir scalable-api
cd scalable-api
npm init -y
npm install express typescript @types/express
npm install -D nodemon ts-node
TypeScript Configuration
Create a robust tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Architecture Patterns
Layered Architecture
Organize your code into clear layers:
src/
├── controllers/
├── services/
├── models/
├── middleware/
├── routes/
└── utils/
Dependency Injection
Use dependency injection for better testability:
export class UserService {
constructor(private userRepository: UserRepository) {}
async createUser(userData: CreateUserDTO): Promise<User> {
return this.userRepository.create(userData);
}
}
Error Handling
Global Error Handler
Implement centralized error handling:
export class AppError extends Error {
public statusCode: number;
public isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
export const globalErrorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
status: 'error',
message: err.message
});
}
console.error(err);
res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
};
Validation and DTOs
Input Validation
Use Joi or Zod for request validation:
import Joi from 'joi';
export const createUserSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
name: Joi.string().min(2).required()
});
export const validateCreateUser = (req: Request, res: Response, next: NextFunction) => {
const { error } = createUserSchema.validate(req.body);
if (error) {
throw new AppError(error.details[0].message, 400);
}
next();
};
Database Integration
Repository Pattern
Abstract database operations:
export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(userData: CreateUserDTO): Promise<User>;
update(id: string, userData: UpdateUserDTO): Promise<User>;
delete(id: string): Promise<void>;
}
export class PrismaUserRepository implements UserRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
// ... other methods
}
Authentication & Authorization
JWT Implementation
Secure your API with JWT:
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
throw new AppError('Access token required', 401);
}
jwt.verify(token, process.env.JWT_SECRET!, (err, user) => {
if (err) {
throw new AppError('Invalid token', 403);
}
req.user = user;
next();
});
};
Caching Strategies
Redis Integration
Implement caching for better performance:
export class CacheService {
constructor(private redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async del(key: string): Promise<void> {
await this.redis.del(key);
}
}
Rate Limiting
Protect Your API
Implement rate limiting to prevent abuse:
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
export const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // stricter limit for sensitive endpoints
message: 'Too many attempts, please try again later'
});
Testing
Unit Testing
Write comprehensive tests:
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockUserRepository = {
findById: jest.fn(),
create: jest.fn(),
// ... other methods
} as jest.Mocked<UserRepository>;
userService = new UserService(mockUserRepository);
});
it('should create a user successfully', async () => {
const userData = { email: 'test@example.com', name: 'Test User' };
const expectedUser = { id: '1', ...userData };
mockUserRepository.create.mockResolvedValue(expectedUser);
const result = await userService.createUser(userData);
expect(result).toEqual(expectedUser);
expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
});
});
Performance Optimization
Database Optimization
Optimize your database queries:
- Use connection pooling
- Implement query optimization
- Add appropriate indexes
- Use database transactions when needed
Response Compression
Enable gzip compression:
import compression from 'compression';
app.use(compression());
Monitoring and Logging
Structured Logging
Implement proper logging:
import winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
Deployment Considerations
Environment Configuration
Use environment-specific configurations:
export const config = {
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL!,
jwtSecret: process.env.JWT_SECRET!,
redisUrl: process.env.REDIS_URL,
environment: process.env.NODE_ENV || 'development'
};
Conclusion
Building scalable APIs requires careful attention to architecture, error handling, security, and performance. By following these practices and using TypeScript for better type safety, you can create robust APIs that scale with your application's needs.
Remember to continuously monitor your API's performance, implement proper testing, and keep security considerations at the forefront of your development process.