REST API Server
Complete example of building a REST API server with Response Handler.
Full Express.js REST API
Server Setup
javascript
const express = require('express');
const { quickSetup } = require('response-handler');
const cors = require('cors');
const helmet = require('helmet');
const app = express();
// Security middleware
app.use(helmet());
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
}),
);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Response Handler middleware
app.use(
quickSetup({
enableLogging: true,
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'info',
environment: process.env.NODE_ENV || 'development',
enablePerformanceTracking: true,
enableSecurity: true,
}),
);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;User Management API
User Model
javascript
// models/User.js
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production',
});
class User {
static async findAll(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const query = `
SELECT id, email, name, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`;
const countQuery = 'SELECT COUNT(*) FROM users';
const [result, countResult] = await Promise.all([
pool.query(query, [limit, offset]),
pool.query(countQuery),
]);
return {
users: result.rows,
total: parseInt(countResult.rows[0].count),
page,
limit,
totalPages: Math.ceil(countResult.rows[0].count / limit),
};
}
static async findById(id) {
const query = 'SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1';
const result = await pool.query(query, [id]);
return result.rows[0];
}
static async create(userData) {
const { email, name, password } = userData;
const query = `
INSERT INTO users (email, name, password_hash, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW())
RETURNING id, email, name, created_at
`;
const result = await pool.query(query, [email, name, password]);
return result.rows[0];
}
static async update(id, userData) {
const { email, name } = userData;
const query = `
UPDATE users
SET email = $1, name = $2, updated_at = NOW()
WHERE id = $3
RETURNING id, email, name, updated_at
`;
const result = await pool.query(query, [email, name, id]);
return result.rows[0];
}
static async delete(id) {
const query = 'DELETE FROM users WHERE id = $1 RETURNING id';
const result = await pool.query(query, [id]);
return result.rows[0];
}
}
module.exports = User;User Routes
javascript
// routes/users.js
const express = require('express');
const bcrypt = require('bcrypt');
const User = require('../models/User');
const { authenticateToken, authorize } = require('../middleware/auth');
const router = express.Router();
// GET /api/users - List users with pagination
router.get('/', authenticateToken, authorize(['admin', 'user']), async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
if (limit > 100) {
return res.badRequest({ limit: 'Maximum limit is 100' }, 'Invalid pagination parameters');
}
const result = await User.findAll(page, limit);
res.ok(result.users, 'Users retrieved successfully', {
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: result.totalPages,
},
});
} catch (error) {
res.error(error, 'Failed to retrieve users');
}
});
// GET /api/users/:id - Get single user
router.get('/:id', authenticateToken, async (req, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.badRequest({ id: 'Invalid user ID format' }, 'User ID must be a number');
}
// Users can only access their own data unless they're admin
if (req.user.role !== 'admin' && req.user.id !== userId) {
return res.forbidden({}, 'Access denied');
}
const user = await User.findById(userId);
if (!user) {
return res.notFound({}, 'User not found');
}
res.ok(user, 'User retrieved successfully');
} catch (error) {
res.error(error, 'Failed to retrieve user');
}
});
// POST /api/users - Create new user
router.post('/', authenticateToken, authorize(['admin']), async (req, res) => {
try {
const { email, name, password } = req.body;
// Validation
if (!email || !name || !password) {
return res.badRequest(
{
missingFields: ['email', 'name', 'password'].filter((field) => !req.body[field]),
},
'Missing required fields',
);
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.badRequest({ email: 'Invalid email format' }, 'Invalid email address');
}
// Password validation
if (password.length < 8) {
return res.badRequest(
{ password: 'Password must be at least 8 characters' },
'Password too short',
);
}
// Check if user already exists
const existingUser = await User.findByEmail(email);
if (existingUser) {
return res.conflict({ email: 'User with this email already exists' }, 'Email already in use');
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create user
const newUser = await User.create({
email,
name,
password: hashedPassword,
});
res.created(newUser, 'User created successfully');
} catch (error) {
res.error(error, 'Failed to create user');
}
});
// PUT /api/users/:id - Update user
router.put('/:id', authenticateToken, async (req, res) => {
try {
const userId = parseInt(req.params.id);
const { email, name } = req.body;
if (isNaN(userId)) {
return res.badRequest({ id: 'Invalid user ID format' }, 'User ID must be a number');
}
// Users can only update their own data unless they're admin
if (req.user.role !== 'admin' && req.user.id !== userId) {
return res.forbidden({}, 'Access denied');
}
// Check if user exists
const existingUser = await User.findById(userId);
if (!existingUser) {
return res.notFound({}, 'User not found');
}
// Validation
if (email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.badRequest({ email: 'Invalid email format' }, 'Invalid email address');
}
}
const updatedUser = await User.update(userId, { email, name });
res.ok(updatedUser, 'User updated successfully');
} catch (error) {
res.error(error, 'Failed to update user');
}
});
// DELETE /api/users/:id - Delete user
router.delete('/:id', authenticateToken, authorize(['admin']), async (req, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.badRequest({ id: 'Invalid user ID format' }, 'User ID must be a number');
}
const user = await User.findById(userId);
if (!user) {
return res.notFound({}, 'User not found');
}
await User.delete(userId);
res.ok({ id: userId }, 'User deleted successfully');
} catch (error) {
res.error(error, 'Failed to delete user');
}
});
module.exports = router;Product Catalog API
Product Routes
javascript
// routes/products.js
const express = require('express');
const Product = require('../models/Product');
const { authenticateToken, authorize } = require('../middleware/auth');
const router = express.Router();
// GET /api/products - List products with filtering and search
router.get('/', async (req, res) => {
try {
const {
page = 1,
limit = 20,
category,
minPrice,
maxPrice,
search,
sortBy = 'created_at',
sortOrder = 'desc',
} = req.query;
const filters = {
category,
minPrice: minPrice ? parseFloat(minPrice) : undefined,
maxPrice: maxPrice ? parseFloat(maxPrice) : undefined,
search,
};
const result = await Product.findAll({
page: parseInt(page),
limit: Math.min(parseInt(limit), 50), // Max 50 per page
filters,
sortBy,
sortOrder,
});
res.ok(result.products, 'Products retrieved successfully', {
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: result.totalPages,
},
filters: Object.fromEntries(
Object.entries(filters).filter(([_, value]) => value !== undefined),
),
});
} catch (error) {
res.error(error, 'Failed to retrieve products');
}
});
// GET /api/products/:id - Get single product
router.get('/:id', async (req, res) => {
try {
const productId = parseInt(req.params.id);
if (isNaN(productId)) {
return res.badRequest({ id: 'Invalid product ID format' }, 'Product ID must be a number');
}
const product = await Product.findById(productId);
if (!product) {
return res.notFound({}, 'Product not found');
}
res.ok(product, 'Product retrieved successfully');
} catch (error) {
res.error(error, 'Failed to retrieve product');
}
});
// POST /api/products - Create new product
router.post('/', authenticateToken, authorize(['admin', 'manager']), async (req, res) => {
try {
const { name, description, price, category, sku, stock } = req.body;
// Validation
const requiredFields = ['name', 'price', 'category'];
const missingFields = requiredFields.filter((field) => !req.body[field]);
if (missingFields.length > 0) {
return res.badRequest({ missingFields }, 'Missing required fields');
}
if (price <= 0) {
return res.badRequest({ price: 'Price must be greater than 0' }, 'Invalid price');
}
if (sku && (await Product.findBySku(sku))) {
return res.conflict({ sku: 'Product with this SKU already exists' }, 'SKU already in use');
}
const newProduct = await Product.create({
name,
description,
price: parseFloat(price),
category,
sku,
stock: parseInt(stock) || 0,
});
res.created(newProduct, 'Product created successfully');
} catch (error) {
res.error(error, 'Failed to create product');
}
});
module.exports = router;Order Management API
Order Processing
javascript
// routes/orders.js
const express = require('express');
const Order = require('../models/Order');
const Product = require('../models/Product');
const { authenticateToken, authorize } = require('../middleware/auth');
const router = express.Router();
// POST /api/orders - Create new order
router.post('/', authenticateToken, async (req, res) => {
try {
const { items, shippingAddress } = req.body;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.badRequest(
{ items: 'Order must contain at least one item' },
'Invalid order items',
);
}
// Validate and calculate order
let totalAmount = 0;
const validatedItems = [];
for (const item of items) {
const { productId, quantity } = item;
if (!productId || !quantity || quantity <= 0) {
return res.badRequest(
{ item: 'Invalid item format' },
'Each item must have productId and positive quantity',
);
}
const product = await Product.findById(productId);
if (!product) {
return res.badRequest(
{ productId: `Product ${productId} not found` },
'Invalid product in order',
);
}
if (product.stock < quantity) {
return res.badRequest(
{
productId,
available: product.stock,
requested: quantity,
},
'Insufficient stock',
);
}
const itemTotal = product.price * quantity;
totalAmount += itemTotal;
validatedItems.push({
productId,
quantity,
price: product.price,
total: itemTotal,
});
}
// Create order
const order = await Order.create({
userId: req.user.id,
items: validatedItems,
totalAmount,
shippingAddress,
status: 'pending',
});
// Update product stock
for (const item of validatedItems) {
await Product.updateStock(item.productId, -item.quantity);
}
res.created(order, 'Order created successfully');
} catch (error) {
res.error(error, 'Failed to create order');
}
});
// GET /api/orders - Get user's orders
router.get('/', authenticateToken, async (req, res) => {
try {
const { page = 1, limit = 10, status } = req.query;
const orders = await Order.findByUserId(req.user.id, {
page: parseInt(page),
limit: parseInt(limit),
status,
});
res.ok(orders.orders, 'Orders retrieved successfully', {
pagination: {
page: orders.page,
limit: orders.limit,
total: orders.total,
totalPages: orders.totalPages,
},
});
} catch (error) {
res.error(error, 'Failed to retrieve orders');
}
});
module.exports = router;Complete API Structure
Main Application
javascript
// app.js
const express = require('express');
const { quickSetup } = require('response-handler');
// Import routes
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const orderRoutes = require('./routes/orders');
const app = express();
// Middleware
app.use(express.json());
app.use(
quickSetup({
enableLogging: true,
logLevel: 'info',
environment: process.env.NODE_ENV || 'development',
}),
);
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);
// Health check
app.get('/health', (req, res) => {
res.ok(
{
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
},
'Service is healthy',
);
});
// 404 handler
app.use('*', (req, res) => {
res.notFound({}, 'Route not found');
});
// Global error handler
app.use((error, req, res, next) => {
console.error('Unhandled error:', error);
res.error(error, 'Internal server error');
});
module.exports = app;Testing the API
API Testing with Jest and Supertest
javascript
// tests/api.test.js
const request = require('supertest');
const app = require('../app');
describe('REST API', () => {
describe('Products API', () => {
it('should get products list', async () => {
const response = await request(app).get('/api/products').expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.pagination).toBeDefined();
});
it('should handle product not found', async () => {
const response = await request(app).get('/api/products/99999').expect(404);
expect(response.body.success).toBe(false);
expect(response.body.message).toBe('Product not found');
});
});
describe('Users API', () => {
it('should require authentication', async () => {
const response = await request(app).get('/api/users').expect(401);
expect(response.body.success).toBe(false);
});
});
});This REST API example demonstrates a complete, production-ready API server with proper validation, error handling, authentication, and Response Handler integration.