Skip to content

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 files

Medium 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/
    └── .gitkeep

Configuration 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=debug

Application 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

  1. Create a project structure for a blog application
  2. Implement a configuration system with validation
  3. Build a modular authentication system
  4. 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

Content is for learning and research only.