Part 7: Testing, Performance, and Production Deployment
The Production Deployment That Went Wrong
My first production deployment: pushed code at 9 AM on a Monday. By 9:30 AM, customer support was flooded with complaints. The API was timing out.
The problem? I'd never tested with production data volumes. My local database had 100 users. Production had 500,000. A query that took 10ms locally took 30 seconds in production—no indexes on the database tables.
Emergency rollback. Spent the day adding indexes and load testing. Redeployed at 5 PM.
Lesson: Testing with realistic data and load conditions isn't optional—it's survival.
This article covers the testing and deployment practices that would have saved me that day.
Testing Strategy
Testing Pyramid
/\
/E2E\ 10% - End-to-end tests
/______\
/ API \ 20% - API/Integration tests
/__________\
/ Unit \ 70% - Unit tests
/________________\
Unit Testing
Test business logic in isolation.
Integration Testing
Test API endpoints with real database (using Testcontainers).
Load Testing
Test API performance under load using k6.
Run load tests:
Performance Optimization
Database Indexing
Query Optimization
Caching
Production Deployment
Dockerfile
Docker Compose
Health Checks
CI/CD Pipeline (GitHub Actions)
Monitoring
Key Takeaways
Test pyramid: 70% unit, 20% integration, 10% E2E
Load test with realistic data volumes
Add database indexes for frequently queried fields
Cache frequently accessed data (Redis)
Use connection pooling for databases
Health checks for liveness and readiness
Docker for consistent deployments
CI/CD for automated testing and deployment
Monitor performance metrics (Prometheus)
Zero-downtime deployments with rolling updates
Series complete! You now have everything needed to build production-ready REST APIs.
Testing and monitoring aren't afterthoughts—they're what separate hobby projects from production systems.
// src/services/__tests__/user-service.test.ts
import { UserService } from '../user-service';
import { UserRepository } from '../../repositories/user-repository';
// Mock repository
jest.mock('../../repositories/user-repository');
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockUserRepository = new UserRepository(null as any) as jest.Mocked<UserRepository>;
userService = new UserService(mockUserRepository);
});
describe('createUser', () => {
it('should create user with valid data', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123',
name: 'John Doe',
};
const expectedUser = {
id: 'user_123',
email: userData.email,
name: userData.name,
createdAt: new Date(),
};
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(expectedUser);
const result = await userService.createUser(userData);
expect(result).toEqual(expectedUser);
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith(userData.email);
expect(mockUserRepository.create).toHaveBeenCalled();
});
it('should throw error if email already exists', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123',
name: 'John Doe',
};
mockUserRepository.findByEmail.mockResolvedValue({
id: 'user_456',
email: userData.email,
name: 'Existing User',
} as any);
await expect(userService.createUser(userData)).rejects.toThrow(
'Email already registered'
);
});
it('should throw error for invalid email', async () => {
const userData = {
email: 'invalid-email',
password: 'SecurePass123',
name: 'John Doe',
};
mockUserRepository.findByEmail.mockResolvedValue(null);
await expect(userService.createUser(userData)).rejects.toThrow(
'Invalid email format'
);
});
it('should throw error for weak password', async () => {
const userData = {
email: '[email protected]',
password: 'weak',
name: 'John Doe',
};
mockUserRepository.findByEmail.mockResolvedValue(null);
await expect(userService.createUser(userData)).rejects.toThrow(
'Password must be at least 8 characters'
);
});
});
describe('getUserById', () => {
it('should return user if found', async () => {
const userId = 'user_123';
const expectedUser = {
id: userId,
email: '[email protected]',
name: 'John Doe',
};
mockUserRepository.findById.mockResolvedValue(expectedUser as any);
const result = await userService.getUserById(userId);
expect(result).toEqual(expectedUser);
expect(mockUserRepository.findById).toHaveBeenCalledWith(userId);
});
it('should throw NotFoundError if user not found', async () => {
const userId = 'nonexistent';
mockUserRepository.findById.mockResolvedValue(null);
await expect(userService.getUserById(userId)).rejects.toThrow('User not found');
});
});
});
npm install -D @testcontainers/postgresql
// src/__tests__/integration/user-api.test.ts
import request from 'supertest';
import { App } from '../../app';
import { Database } from '../../config/database';
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
describe('User API Integration Tests', () => {
let app: any;
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
// Start PostgreSQL container
container = await new PostgreSqlContainer('postgres:15').start();
// Override database config
process.env.DB_HOST = container.getHost();
process.env.DB_PORT = container.getPort().toString();
process.env.DB_NAME = container.getDatabase();
process.env.DB_USER = container.getUsername();
process.env.DB_PASSWORD = container.getPassword();
// Initialize app
app = new App().getApp();
// Run migrations
await Database.getInstance().query(`
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
`);
}, 60000);
afterAll(async () => {
await Database.close();
await container.stop();
});
beforeEach(async () => {
// Clean database before each test
await Database.getInstance().query('DELETE FROM users');
});
describe('POST /api/v1/users', () => {
it('should create user with valid data', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123',
name: 'John Doe',
};
const response = await request(app)
.post('/api/v1/users')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toMatchObject({
email: userData.email,
name: userData.name,
});
expect(response.body.data.id).toBeDefined();
expect(response.body.data.password).toBeUndefined(); // Password should not be returned
});
it('should return 422 for invalid email', async () => {
const userData = {
email: 'invalid-email',
password: 'SecurePass123',
name: 'John Doe',
};
const response = await request(app)
.post('/api/v1/users')
.send(userData)
.expect(422);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Validation failed');
});
it('should return 409 for duplicate email', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123',
name: 'John Doe',
};
// Create first user
await request(app).post('/api/v1/users').send(userData).expect(201);
// Try to create duplicate
const response = await request(app)
.post('/api/v1/users')
.send(userData)
.expect(409);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('already registered');
});
});
describe('GET /api/v1/users/:id', () => {
it('should return user if exists', async () => {
// Create user
const createResponse = await request(app)
.post('/api/v1/users')
.send({
email: '[email protected]',
password: 'SecurePass123',
name: 'John Doe',
});
const userId = createResponse.body.data.id;
// Get user
const response = await request(app)
.get(`/api/v1/users/${userId}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe(userId);
});
it('should return 404 if user not found', async () => {
const response = await request(app)
.get('/api/v1/users/nonexistent')
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('not found');
});
});
});
npm install -D k6
// tests/load/user-api-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 }, // Ramp up to 50 users
{ duration: '1m', target: 50 }, // Stay at 50 users
{ duration: '30s', target: 100 }, // Ramp up to 100 users
{ duration: '1m', target: 100 }, // Stay at 100 users
{ duration: '30s', target: 0 }, // Ramp down to 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests must complete within 500ms
http_req_failed: ['rate<0.01'], // Error rate must be below 1%
},
};
const BASE_URL = 'http://localhost:4000/api/v1';
export default function () {
// Test GET /users
const getResponse = http.get(`${BASE_URL}/users`);
check(getResponse, {
'GET /users status is 200': (r) => r.status === 200,
'GET /users response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
// Test POST /users
const userData = {
email: `user${Date.now()}@example.com`,
password: 'SecurePass123',
name: 'Load Test User',
};
const postResponse = http.post(
`${BASE_URL}/users`,
JSON.stringify(userData),
{
headers: { 'Content-Type': 'application/json' },
}
);
check(postResponse, {
'POST /users status is 201': (r) => r.status === 201,
'POST /users response time < 1000ms': (r) => r.timings.duration < 1000,
});
sleep(1);
}
k6 run tests/load/user-api-load.js
-- Add indexes for frequently queried fields
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_created_at ON users(created_at);
-- Composite index for common query patterns
CREATE INDEX idx_users_role_created ON users(role, created_at);
-- Analyze query performance
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
// BAD: N+1 query problem
export class OrderService {
async getOrdersWithUsers(): Promise<any[]> {
const orders = await this.orderRepository.findAll();
// This fires one query per order!
for (const order of orders) {
order.user = await this.userRepository.findById(order.userId);
}
return orders;
}
}
// GOOD: Join or batch fetch
export class OrderService {
async getOrdersWithUsers(): Promise<any[]> {
// Single query with join
return this.orderRepository.findAllWithUsers();
}
}
// In repository
async findAllWithUsers(): Promise<Order[]> {
const result = await this.db.query(`
SELECT
o.*,
json_build_object(
'id', u.id,
'name', u.name,
'email', u.email
) as user
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
`);
return result.rows;
}