Building a REST API Project - Part 3
Building a Complete REST API Project - Part 3
In this final lesson of our REST API project series, we'll add comprehensive testing, API documentation, enhanced security hardening, and deploy our application. This will transform our project into a production-ready REST API.
Step 1: Setting Up Testing Environment
Install testing dependencies:
npm install --save-dev jest supertest @types/jest
npm install --save-dev mongodb-memory-server
Update package.json with test scripts:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest --watchAll --verbose",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"]
}
Step 2: Test Setup
Create test setup file (tests/setup.js):
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
// Connect to in-memory database before tests
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
// Clear database between tests
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
// Disconnect and stop in-memory database after tests
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
module.exports = { mongoServer };
Step 3: Authentication Tests
Create authentication tests (tests/auth.test.js):
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
require('./setup');
describe('Authentication Endpoints', () => {
describe('POST /api/auth/register', () => {
it('should register a new user successfully', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user).toHaveProperty('email', 'john@example.com');
expect(response.body.data).toHaveProperty('token');
expect(response.body.data.user).not.toHaveProperty('password');
});
it('should fail with duplicate email', async () => {
await User.create({
name: 'Jane Doe',
email: 'jane@example.com',
password: 'password123'
});
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'Another User',
email: 'jane@example.com',
password: 'password456'
})
.expect(409);
expect(response.body.success).toBe(false);
});
it('should fail with invalid data', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'J',
email: 'invalid-email',
password: '123'
})
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await request(app)
.post('/api/auth/register')
.send({
name: 'Test User',
email: 'test@example.com',
password: 'password123'
});
});
it('should login successfully with correct credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('token');
});
it('should fail with incorrect password', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
})
.expect(401);
expect(response.body.success).toBe(false);
});
it('should fail with non-existent user', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123'
})
.expect(401);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/auth/profile', () => {
let token;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'Profile User',
email: 'profile@example.com',
password: 'password123'
});
token = response.body.data.token;
});
it('should get user profile with valid token', async () => {
const response = await request(app)
.get('/api/auth/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('email', 'profile@example.com');
});
it('should fail without token', async () => {
await request(app)
.get('/api/auth/profile')
.expect(401);
});
it('should fail with invalid token', async () => {
await request(app)
.get('/api/auth/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});
Step 4: Task Tests
Create task tests (tests/tasks.test.js):
const request = require('supertest');
const app = require('../src/app');
require('./setup');
describe('Task Endpoints', () => {
let token;
let userId;
beforeEach(async () => {
// Register and login user
const response = await request(app)
.post('/api/auth/register')
.send({
name: 'Task User',
email: 'taskuser@example.com',
password: 'password123'
});
token = response.body.data.token;
userId = response.body.data.user._id;
});
describe('POST /api/tasks', () => {
it('should create a new task', async () => {
const response = await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Test Task',
description: 'This is a test task',
status: 'pending',
priority: 'high'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('title', 'Test Task');
expect(response.body.data).toHaveProperty('userId', userId);
});
it('should fail without authentication', async () => {
await request(app)
.post('/api/tasks')
.send({
title: 'Test Task'
})
.expect(401);
});
it('should fail with invalid data', async () => {
const response = await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'ab' // Too short
})
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/tasks', () => {
beforeEach(async () => {
// Create test tasks
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Task 1',
status: 'pending',
priority: 'high'
});
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Task 2',
status: 'completed',
priority: 'low'
});
});
it('should get all tasks', async () => {
const response = await request(app)
.get('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.tasks).toHaveLength(2);
expect(response.body.data).toHaveProperty('pagination');
});
it('should filter tasks by status', async () => {
const response = await request(app)
.get('/api/tasks?status=pending')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.data.tasks).toHaveLength(1);
expect(response.body.data.tasks[0]).toHaveProperty('status', 'pending');
});
it('should paginate tasks', async () => {
const response = await request(app)
.get('/api/tasks?page=1&limit=1')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.data.tasks).toHaveLength(1);
expect(response.body.data.pagination).toHaveProperty('totalPages', 2);
});
});
describe('GET /api/tasks/statistics', () => {
it('should get task statistics', async () => {
await request(app)
.post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Test Task',
status: 'pending'
});
const response = await request(app)
.get('/api/tasks/statistics')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('total', 1);
expect(response.body.data).toHaveProperty('byStatus');
expect(response.body.data.byStatus).toHaveProperty('pending', 1);
});
});
});
mongodb-memory-server to run tests in an isolated in-memory database. This ensures tests don't affect your development or production database.Step 5: API Documentation with Swagger
Install Swagger dependencies:
npm install swagger-jsdoc swagger-ui-express
Create Swagger configuration (src/config/swagger.js):
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Task Management API',
version: '1.0.0',
description: 'A comprehensive task management REST API with authentication, categories, and file attachments',
contact: {
name: 'API Support',
email: 'support@taskmanager.com'
},
},
servers: [
{
url: 'http://localhost:5000',
description: 'Development server'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
User: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['user', 'admin'] },
isActive: { type: 'boolean' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Task: {
type: 'object',
properties: {
_id: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
status: {
type: 'string',
enum: ['pending', 'in-progress', 'completed', 'cancelled']
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'urgent']
},
dueDate: { type: 'string', format: 'date-time' },
categoryId: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
userId: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Error: {
type: 'object',
properties: {
success: { type: 'boolean', example: false },
message: { type: 'string' }
}
}
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = specs;
Add Swagger to app.js:
const swaggerUi = require('swagger-ui-express');
const swaggerSpecs = require('./config/swagger');
// API Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
Add Swagger annotations to routes (src/routes/auth.js):
/**
* @swagger
* /api/auth/register:
* post:
* summary: Register a new user
* tags: [Authentication]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* example: John Doe
* email:
* type: string
* format: email
* example: john@example.com
* password:
* type: string
* format: password
* minLength: 6
* example: password123
* responses:
* 201:
* description: User registered successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* user:
* $ref: '#/components/schemas/User'
* token:
* type: string
* 400:
* description: Validation error
* 409:
* description: User already exists
*/
router.post('/register', registerValidation, validate, authController.register);
Step 6: Rate Limiting
Install rate limiting middleware:
npm install express-rate-limit
Create rate limiter middleware (src/middleware/rateLimiter.js):
const rateLimit = require('express-rate-limit');
// General API limiter
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, please try again later'
});
// Strict limiter for authentication routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
skipSuccessfulRequests: true,
message: 'Too many authentication attempts, please try again later'
});
// File upload limiter
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 uploads per hour
message: 'Too many file uploads, please try again later'
});
module.exports = { apiLimiter, authLimiter, uploadLimiter };
Apply rate limiters in app.js:
const { apiLimiter, authLimiter } = require('./middleware/rateLimiter');
// Apply rate limiting
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
Step 7: Request Logging
Create custom request logger (src/middleware/requestLogger.js):
const fs = require('fs');
const path = require('path');
// Create logs directory if it doesn't exist
const logsDir = path.join(__dirname, '../../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
const requestLogger = (req, res, next) => {
const start = Date.now();
// Log response
res.on('finish', () => {
const duration = Date.now() - start;
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
};
// Log to file
const logFile = path.join(logsDir, `${new Date().toISOString().split('T')[0]}.log`);
fs.appendFileSync(logFile, JSON.stringify(log) + '\n');
// Log errors to console
if (res.statusCode >= 400) {
console.error('Error:', log);
}
});
next();
};
module.exports = requestLogger;
Step 8: Input Sanitization
Install sanitization packages:
npm install express-mongo-sanitize xss-clean
Add sanitization middleware to app.js:
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
// Data sanitization against NoSQL injection
app.use(mongoSanitize());
// Data sanitization against XSS
app.use(xss());
Step 9: Environment-Based Configuration
Create configuration file (src/config/config.js):
module.exports = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 5000,
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 10,
maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 5242880,
allowedFileTypes: process.env.ALLOWED_FILE_TYPES?.split(',') || [
'image/jpeg',
'image/png',
'application/pdf'
],
corsOrigin: process.env.CORS_ORIGIN || '*',
logLevel: process.env.LOG_LEVEL || 'info'
};
Step 10: Production Deployment Checklist
Create a comprehensive deployment checklist:
# Production Environment Variables
NODE_ENV=production
PORT=5000
MONGODB_URI=mongodb://production-server/taskmanagement
JWT_SECRET=your-production-secret-key-here
JWT_EXPIRES_IN=7d
BCRYPT_ROUNDS=12
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=image/jpeg,image/png,application/pdf
CORS_ORIGIN=https://yourdomain.com
# Production Checklist:
# 1. Set strong JWT_SECRET
# 2. Use production MongoDB server
# 3. Enable HTTPS
# 4. Configure CORS for specific domain
# 5. Set up process manager (PM2)
# 6. Configure reverse proxy (Nginx)
# 7. Set up monitoring and logging
# 8. Enable backups
# 9. Configure firewall rules
# 10. Set up SSL certificates
Install PM2 for process management:
npm install -g pm2
# Start application
pm2 start server.js --name task-api
# Monitor application
pm2 monit
# View logs
pm2 logs task-api
# Restart application
pm2 restart task-api
# Save PM2 configuration
pm2 save
# Set PM2 to start on system boot
pm2 startup
Practice Exercise
Complete the following tasks:
- Run all tests and ensure they pass:
npm test - Check test coverage:
npm run test:coverage - Access API documentation at http://localhost:5000/api-docs
- Test rate limiting by making multiple rapid requests
- Add Swagger documentation for task routes
- Test input sanitization with malicious payloads
- Review security headers using browser developer tools
- Set up PM2 and test process restart
Security Best Practices Summary
Our API now implements comprehensive security measures:
- Authentication: JWT-based authentication with bcrypt password hashing
- Authorization: Role-based access control and user ownership validation
- Rate Limiting: Protection against brute force and DoS attacks
- Input Validation: express-validator for all user inputs
- Input Sanitization: Protection against NoSQL injection and XSS
- Security Headers: Helmet middleware for HTTP security headers
- CORS: Configurable cross-origin resource sharing
- File Upload Security: File type and size validation
- Error Handling: No sensitive data exposure in error messages
- Logging: Comprehensive request and error logging
Summary
In this final lesson, we completed our REST API project with:
- Comprehensive automated testing with Jest and Supertest
- In-memory database testing with mongodb-memory-server
- Interactive API documentation with Swagger/OpenAPI
- Rate limiting for DDoS and brute force protection
- Request logging for monitoring and debugging
- Input sanitization against injection attacks
- Environment-based configuration management
- Production deployment checklist and PM2 setup
- Comprehensive security hardening
You now have a production-ready REST API with authentication, CRUD operations, file uploads, pagination, filtering, testing, documentation, and security best practices. This project demonstrates professional-level API development skills that are highly valued in the industry.