You’ve built a functional application with Sequelize and PostgreSQL. Now it’s time to make it production-ready. This final part covers advanced patterns including error handling, input validation, transactions, connection pooling optimization, and performance monitoring. These practices ensure your application is resilient, secure, and scalable.
Error Handling Middleware
Create src/middleware/errorHandler.js to centralize error handling:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal server error';
console.error(`Error at ${new Date().toISOString()}:`, err);
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({
success: false,
message: 'This email already exists',
error: 'UNIQUE_CONSTRAINT_ERROR',
});
}
if (err.name === 'SequelizeValidationError') {
return res.status(400).json({
success: false,
message: 'Validation error',
errors: err.errors.map((e) => ({
field: e.path,
message: e.message,
})),
});
}
if (err.name === 'SequelizeForeignKeyConstraintError') {
return res.status(400).json({
success: false,
message: 'Invalid reference to related data',
error: 'FOREIGN_KEY_ERROR',
});
}
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = {
AppError,
errorHandler,
asyncHandler,
};
Input Validation
Install validation package:
npm install express-validator
Create src/validators/userValidator.js:
const { body, validationResult } = require('express-validator');
const validateCreateUser = [
body('firstName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('First name must be 2-50 characters'),
body('lastName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Last name must be 2-50 characters'),
body('email')
.isEmail()
.withMessage('Invalid email format')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
.matches(/[A-Z]/)
.withMessage('Password must contain uppercase letter')
.matches(/[0-9]/)
.withMessage('Password must contain number'),
];
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array(),
});
}
next();
};
module.exports = {
validateCreateUser,
handleValidationErrors,
};
Database Transactions
Create src/services/transactionService.js for complex multi-step operations:
const sequelize = require('../config/sequelize');
const { User, Post } = require('../models');
class TransactionService {
async transferUserContent(fromUserId, toUserId) {
const transaction = await sequelize.transaction();
try {
const fromUser = await User.findByPk(fromUserId, { transaction });
const toUser = await User.findByPk(toUserId, { transaction });
if (!fromUser || !toUser) {
throw new Error('User not found');
}
const posts = await Post.findAll(
{ where: { userId: fromUserId } },
{ transaction }
);
for (const post of posts) {
await post.update({ userId: toUserId }, { transaction });
}
await fromUser.destroy({ transaction });
await transaction.commit();
return { message: 'Content transferred successfully' };
} catch (error) {
await transaction.rollback();
throw new Error(`Transaction failed: ${error.message}`);
}
}
async createUserWithInitialPost(userData, postData) {
const transaction = await sequelize.transaction();
try {
const user = await User.create(userData, { transaction });
postData.userId = user.id;
const post = await Post.create(postData, { transaction });
await transaction.commit();
return { user, post };
} catch (error) {
await transaction.rollback();
throw new Error(`Failed to create user with post: ${error.message}`);
}
}
}
module.exports = new TransactionService();
Connection Pool Optimization
Update src/config/database.js for production optimization:
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 1,
acquire: 30000,
idle: 10000,
evict: 1000,
},
},
production: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'postgres',
logging: false,
pool: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000,
evict: 1000,
},
ssl: {
require: true,
rejectUnauthorized: false,
},
},
};
Query Indexing Strategy
Create src/migrations/add-indexes.js to optimize frequently queried columns:
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addIndex('Users', ['email']);
await queryInterface.addIndex('Posts', ['userId']);
await queryInterface.addIndex('Posts', ['createdAt']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeIndex('Users', ['email']);
await queryInterface.removeIndex('Posts', ['userId']);
await queryInterface.removeIndex('Posts', ['createdAt']);
},
};
Request Logging Middleware
Create src/middleware/logger.js for request tracking:
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.path} ${res.statusCode} ${duration}ms`
);
});
next();
};
module.exports = requestLogger;
Updated Main Application
Update src/index.js with all middleware and error handling:
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const sequelize = require('./config/sequelize');
const userRoutes = require('./routes/userRoutes');
const postRoutes = require('./routes/postRoutes');
const requestLogger = require('./middleware/logger');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(requestLogger);
app.get('/health', (req, res) => {
res.json({ status: 'Server is running', env: process.env.NODE_ENV });
});
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});
app.use(errorHandler);
const startServer = async () => {
try {
await sequelize.authenticate();
console.log('Database connected successfully');
await sequelize.sync({ alter: false });
console.log('Database synchronized');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
startServer();
Updated User Routes with Validation
Update src/routes/userRoutes.js to include validation:
const express = require('express');
const userController = require('../controllers/userController');
const { validateCreateUser, handleValidationErrors } = require('../validators/userValidator');
const { asyncHandler } = require('../middleware/errorHandler');
const router = express.Router();
router.post('/', validateCreateUser, handleValidationErrors, asyncHandler(userController.create));
router.get('/', asyncHandler(userController.getAll));
router.get('/:id', asyncHandler(userController.getById));
router.put('/:id', asyncHandler(userController.update));
router.delete('/:id', asyncHandler(userController.delete));
module.exports = router;
Environment Variables for Production
Update your .env file with production settings:
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres_password
DB_NAME=sequelize_dev_db
LOG_LEVEL=info
API_RATE_LIMIT=100
Database Connection Flow
graph TB subgraph Client["Client Request"] HTTP["HTTP Request"] end subgraph Middleware["Middleware Layer"] Log["Logger"] Validate["Validator"] Error["Error Handler"] end subgraph Business["Business Layer"] Route["Routes"] Controller["Controller"] Service["Service"] end subgraph Data["Data Layer"] Model["Model"] Pool["Connection Pool"] DB["PostgreSQL"] end HTTP -->|Request| Log Log --> Validate Validate --> Route Route --> Controller Controller --> Service Service -->|Transaction| Model Model -->|Query| Pool Pool -->|Execute| DB DB -->|Result| Pool Pool -->|Response| Service Service -->|Data| Controller Controller -->|JSON| Error Error -->|Response| HTTP style Client fill:#61dafb,stroke:#000,color:#000 style Middleware fill:#90c53f,stroke:#000,color:#000 style Business fill:#f39c12,stroke:#000,color:#000 style Data fill:#e74c3c,stroke:#fff,color:#fff
Performance Best Practices
- Use connection pooling with appropriate min and max values based on your workload
- Create indexes on foreign keys and frequently queried columns
- Use raw queries only when absolutely necessary for complex operations
- Implement pagination to avoid loading large datasets
- Cache frequently accessed data when applicable
- Monitor slow queries and optimize them proactively
Security Considerations
- Never store plain text passwords, use bcrypt or similar hashing algorithms
- Always validate and sanitize user inputs
- Use environment variables for sensitive configuration
- Enable SSL connections in production
- Use helmet middleware to set secure HTTP headers
- Implement rate limiting to prevent abuse
- Use parameterized queries (which Sequelize does automatically)
Deployment Checklist
- Set NODE_ENV to ‘production’
- Update database connection pool for production load
- Enable SSL for database connections
- Set up proper logging and monitoring
- Configure environment variables securely
- Run database migrations before deployment
- Set up automated backups for PostgreSQL
- Use process manager like PM2 or Docker
- Configure reverse proxy like Nginx
- Set up monitoring and alerting
Testing Your Application
Test error handling with invalid data:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"firstName": "J",
"lastName": "D",
"email": "invalid-email",
"password": "weak"
}'
Test transaction safety:
const transactionService = require('./services/transactionService'); transactionService.createUserWithInitialPost(
{
firstName: 'Alice',
lastName: 'Smith',
email: 'alice@example.com',
password: 'SecurePass123'
},
{
title: 'Welcome Post',
content: 'Welcome to my blog'
}).then(result =>
console.log(result)) .catch(error => console.error(error)
);
Monitoring and Maintenance
Monitor database connection pool health, query performance, and application logs regularly. Use tools like Prometheus and Grafana for metrics visualization. Set up alerts for slow queries and connection pool exhaustion. Perform regular backups and test recovery procedures.
Key Takeaways from the Series
- Part 1 established a solid foundation with Sequelize and PostgreSQL connection
- Part 2 covered data modeling with associations and migrations
- Part 3 implemented CRUD operations with clean architecture patterns
- Part 4 adds production-ready features including error handling, validation, and optimization
Next Steps for Advancement
Consider these enhancements for your application:
- Add JWT authentication for securing endpoints
- Implement caching with Redis
- Add comprehensive test coverage with Jest
- Set up CI/CD pipeline with GitHub Actions
- Document API with Swagger/OpenAPI
- Implement GraphQL as alternative to REST
- Add database audit trails
References
- Sequelize Transactions: https://sequelize.org/docs/v6/other-topics/transactions/
- Express Middleware: https://expressjs.com/en/guide/using-middleware.html
- PostgreSQL Best Practices: https://wiki.postgresql.org/wiki/Performance_Optimization
- Node.js Production Best Practices: https://nodejs.org/en/docs/guides/nodejs-performance/
- Express Validator: https://express-validator.github.io/docs/