Project Structure
Overview
Organizing your Node.js project properly is crucial for maintainability, scalability, and team collaboration. This chapter covers best practices for structuring Node.js applications, from simple scripts to complex enterprise applications.
Basic Project Structure
Simple Application Structure
For small applications or learning projects:
my-node-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env
├── README.md
├── app.js # Main application file
├── config/
│ └── database.js # Configuration files
├── routes/
│ ├── index.js # Route definitions
│ └── users.js
├── models/
│ └── user.js # Data models
├── views/
│ └── index.html # Templates (if using)
├── public/
│ ├── css/
│ ├── js/
│ └── images/
└── tests/
└── app.test.js # Test filesMedium Application Structure
For production applications:
enterprise-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── README.md
├── Dockerfile
├── docker-compose.yml
├── src/
│ ├── app.js # Application entry point
│ ├── server.js # Server setup
│ ├── config/
│ │ ├── index.js # Configuration management
│ │ ├── database.js
│ │ └── redis.js
│ ├── controllers/
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ ├── validation.js
│ │ └── errorHandler.js
│ ├── models/
│ │ ├── index.js
│ │ ├── User.js
│ │ └── Product.js
│ ├── routes/
│ │ ├── index.js
│ │ ├── auth.js
│ │ └── api/
│ │ ├── v1/
│ │ │ ├── index.js
│ │ │ ├── users.js
│ │ │ └── products.js
│ ├── services/
│ │ ├── authService.js
│ │ ├── emailService.js
│ │ └── paymentService.js
│ ├── utils/
│ │ ├── logger.js
│ │ ├── helpers.js
│ │ └── constants.js
│ └── validators/
│ ├── userValidator.js
│ └── productValidator.js
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── docs/
│ ├── api.md
│ └── deployment.md
├── scripts/
│ ├── seed.js
│ └── migrate.js
└── logs/
└── .gitkeepConfiguration Management
Environment-based Configuration
Create a robust configuration system:
javascript
// src/config/index.js
const dotenv = require('dotenv');
const path = require('path');
// Load environment variables
dotenv.config();
const config = {
// Environment
NODE_ENV: process.env.NODE_ENV || 'development',
// Server
PORT: parseInt(process.env.PORT, 10) || 3000,
HOST: process.env.HOST || 'localhost',
// Database
DATABASE: {
HOST: process.env.DB_HOST || 'localhost',
PORT: parseInt(process.env.DB_PORT, 10) || 5432,
NAME: process.env.DB_NAME || 'myapp',
USER: process.env.DB_USER || 'postgres',
PASSWORD: process.env.DB_PASSWORD || '',
SSL: process.env.DB_SSL === 'true'
},
// Redis
REDIS: {
HOST: process.env.REDIS_HOST || 'localhost',
PORT: parseInt(process.env.REDIS_PORT, 10) || 6379,
PASSWORD: process.env.REDIS_PASSWORD || ''
},
// JWT
JWT: {
SECRET: process.env.JWT_SECRET || 'your-secret-key',
EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h'
},
// Email
EMAIL: {
SERVICE: process.env.EMAIL_SERVICE || 'gmail',
USER: process.env.EMAIL_USER || '',
PASSWORD: process.env.EMAIL_PASSWORD || ''
},
// File uploads
UPLOAD: {
MAX_SIZE: parseInt(process.env.UPLOAD_MAX_SIZE, 10) || 5 * 1024 * 1024, // 5MB
ALLOWED_TYPES: (process.env.UPLOAD_ALLOWED_TYPES || 'jpg,jpeg,png,pdf').split(',')
},
// Logging
LOG_LEVEL: process.env.LOG_LEVEL || 'info'
};
// Validation
function validateConfig() {
const required = ['JWT.SECRET'];
for (const key of required) {
const keys = key.split('.');
let value = config;
for (const k of keys) {
value = value[k];
}
if (!value) {
throw new Error(`Missing required configuration: ${key}`);
}
}
}
// Validate in production
if (config.NODE_ENV === 'production') {
validateConfig();
}
module.exports = config;Environment Files
Create different environment files:
bash
# .env.example
NODE_ENV=development
PORT=3000
HOST=localhost
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=postgres
DB_PASSWORD=password
DB_SSL=false
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h
# Email
EMAIL_SERVICE=gmail
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
# File uploads
UPLOAD_MAX_SIZE=5242880
UPLOAD_ALLOWED_TYPES=jpg,jpeg,png,pdf
# Logging
LOG_LEVEL=debugApplication Entry Points
Server Setup
javascript
// src/server.js
const app = require('./app');
const config = require('./config');
const logger = require('./utils/logger');
const server = app.listen(config.PORT, config.HOST, () => {
logger.info(`Server running on ${config.HOST}:${config.PORT} in ${config.NODE_ENV} mode`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received. Shutting down gracefully...');
server.close(() => {
logger.info('Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
logger.info('SIGINT received. Shutting down gracefully...');
server.close(() => {
logger.info('Process terminated');
process.exit(0);
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
module.exports = server;Application Setup
javascript
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const config = require('./config');
const logger = require('./utils/logger');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: config.NODE_ENV === 'production' ? ['https://yourdomain.com'] : true,
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
app.use(compression());
// Request logging
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path} - ${req.ip}`);
next();
});
// Static files
app.use(express.static('public'));
// Routes
app.use('/', routes);
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found'
});
});
// Error handling middleware
app.use(errorHandler);
module.exports = app;Modular Architecture
Controllers
javascript
// src/controllers/userController.js
const userService = require('../services/userService');
const { validationResult } = require('express-validator');
const logger = require('../utils/logger');
class UserController {
async getAllUsers(req, res, next) {
try {
const { page = 1, limit = 10, search } = req.query;
const users = await userService.getAllUsers({
page: parseInt(page),
limit: parseInt(limit),
search
});
res.json({
success: true,
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: users.total
}
});
} catch (error) {
next(error);
}
}
async getUserById(req, res, next) {
try {
const { id } = req.params;
const user = await userService.getUserById(id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
}
async createUser(req, res, next) {
try {
// Check validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
const userData = req.body;
const user = await userService.createUser(userData);
logger.info(`User created: ${user.id}`);
res.status(201).json({
success: true,
data: user,
message: 'User created successfully'
});
} catch (error) {
next(error);
}
}
async updateUser(req, res, next) {
try {
const { id } = req.params;
const updateData = req.body;
const user = await userService.updateUser(id, updateData);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user,
message: 'User updated successfully'
});
} catch (error) {
next(error);
}
}
async deleteUser(req, res, next) {
try {
const { id } = req.params;
const deleted = await userService.deleteUser(id);
if (!deleted) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
next(error);
}
}
}
module.exports = new UserController();Services
javascript
// src/services/userService.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
const logger = require('../utils/logger');
class UserService {
async getAllUsers({ page = 1, limit = 10, search }) {
try {
const offset = (page - 1) * limit;
let whereClause = {};
if (search) {
whereClause = {
$or: [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
]
};
}
const [users, total] = await Promise.all([
User.find(whereClause)
.select('-password')
.skip(offset)
.limit(limit)
.sort({ createdAt: -1 }),
User.countDocuments(whereClause)
]);
return {
users,
total,
pages: Math.ceil(total / limit)
};
} catch (error) {
logger.error('Error fetching users:', error);
throw error;
}
}
async getUserById(id) {
try {
const user = await User.findById(id).select('-password');
return user;
} catch (error) {
logger.error(`Error fetching user ${id}:`, error);
throw error;
}
}
async createUser(userData) {
try {
// Check if user already exists
const existingUser = await User.findOne({ email: userData.email });
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(userData.password, saltRounds);
// Create user
const user = new User({
...userData,
password: hashedPassword
});
await user.save();
// Return user without password
const { password, ...userWithoutPassword } = user.toObject();
return userWithoutPassword;
} catch (error) {
logger.error('Error creating user:', error);
throw error;
}
}
async updateUser(id, updateData) {
try {
// Remove sensitive fields from update
const { password, ...safeUpdateData } = updateData;
const user = await User.findByIdAndUpdate(
id,
safeUpdateData,
{ new: true, runValidators: true }
).select('-password');
return user;
} catch (error) {
logger.error(`Error updating user ${id}:`, error);
throw error;
}
}
async deleteUser(id) {
try {
const user = await User.findByIdAndDelete(id);
return !!user;
} catch (error) {
logger.error(`Error deleting user ${id}:`, error);
throw error;
}
}
async getUserByEmail(email) {
try {
const user = await User.findOne({ email });
return user;
} catch (error) {
logger.error(`Error fetching user by email ${email}:`, error);
throw error;
}
}
}
module.exports = new UserService();Middleware
javascript
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');
const userService = require('../services/userService');
const authMiddleware = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const decoded = jwt.verify(token, config.JWT.SECRET);
const user = await userService.getUserById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token. User not found.'
});
}
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Invalid token.'
});
}
};
const adminMiddleware = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: 'Access denied. Admin privileges required.'
});
}
next();
};
module.exports = {
authMiddleware,
adminMiddleware
};Utilities and Helpers
Logger
javascript
// src/utils/logger.js
const winston = require('winston');
const config = require('../config');
const logger = winston.createLogger({
level: config.LOG_LEVEL,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'node-app' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Add console transport in development
if (config.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;Helpers
javascript
// src/utils/helpers.js
const crypto = require('crypto');
/**
* Generate random string
*/
function generateRandomString(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
/**
* Sanitize user input
*/
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
return input.trim().replace(/[<>]/g, '');
}
/**
* Format response
*/
function formatResponse(success, data = null, message = null, errors = null) {
const response = { success };
if (data !== null) response.data = data;
if (message !== null) response.message = message;
if (errors !== null) response.errors = errors;
return response;
}
/**
* Pagination helper
*/
function getPaginationData(page, limit, total) {
const totalPages = Math.ceil(total / limit);
const hasNext = page < totalPages;
const hasPrev = page > 1;
return {
page,
limit,
total,
totalPages,
hasNext,
hasPrev
};
}
/**
* Async wrapper for route handlers
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = {
generateRandomString,
sanitizeInput,
formatResponse,
getPaginationData,
asyncHandler
};Package.json Scripts
json
{
"name": "node-enterprise-app",
"version": "1.0.0",
"description": "Enterprise Node.js application",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write src/",
"seed": "node scripts/seed.js",
"migrate": "node scripts/migrate.js",
"build": "echo 'No build step required'",
"docker:build": "docker build -t node-app .",
"docker:run": "docker run -p 3000:3000 node-app"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.0.0",
"bcrypt": "^5.1.0",
"jsonwebtoken": "^9.0.0",
"cors": "^2.8.5",
"helmet": "^6.0.1",
"compression": "^1.7.4",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.15.0",
"winston": "^3.8.2",
"dotenv": "^16.0.3"
},
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0",
"supertest": "^6.3.3",
"eslint": "^8.38.0",
"prettier": "^2.8.7"
}
}Next Steps
In the next chapter, we'll explore configuration management in detail and learn how to handle different environments effectively.
Practice Exercises
- Create a project structure for a blog application
- Implement a configuration system with validation
- Build a modular authentication system
- Create reusable middleware for logging and error handling
Key Takeaways
- Proper project structure improves maintainability and scalability
- Separate concerns using controllers, services, and middleware
- Use environment-based configuration for different deployment scenarios
- Implement proper error handling and logging
- Create reusable utilities and helpers
- Follow consistent naming conventions and file organization