Node.js Project Structure: Best Practices and Example for Clean Code

Jay Jethava
7 min readJust now

--

Discover the top best practices for organizing a Node.js project to improve scalability, maintainability, and readability with a well-planned folder.

Table of Contents:

1. Setting Up the Project

  • Installing Dependencies
  • Initializing a Node.js Application
  • Creating the .env and .gitignore Files

2. Project Folder Structure

  • Organizing the src Folder
  • Explanation of server.js and app.js
  • Introduction to the modules Folder

3. Configuring Express in app.js

  • Setting Up Middleware
  • Enabling CORS and JSON Parsing

4. Creating the Server in server.js

  • Setting Up an HTTP Server
  • Explanation of Environment Variables and Ports

5. Database Configuration

  • Setting Up Environment Variables for Database Credentials
  • Installing mysql2 for Database Connection
  • Creating configs/db.js for Database Setup

6. Introducing the User Module

  • Creating the user Folder in the modules Directory
  • Defining the User Model
  • Implementing User Services
  • Creating User Controllers
  • Defining User Routes

7. Integrating the User Module into app.js

  • Registering Routes in Express

8. Running the Application

  • Defining the Start Script in package.json
  • Starting the Node Server

To get started with a Node.js application using a modular project structure, follow these steps:

Step 1: Install Required Dependencies

First, you’ll need to install the necessary dependencies for your project:

npm install express dotenv sequelize cors mysql2

Step 2: Create a .env File

Next, create a .env file at the root level of your project. This file will hold all your configuration constants, such as API keys, database credentials, and other environment-specific variables. Here’s an example:

PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=mysecretkey

Step 3: Set Up .gitignore for Security

It’s crucial to ensure sensitive information is not pushed to version control, especially when using GitHub. Create a .gitignore file in the root of your project and add the following:

.env
node_modules/
  • Why .env? Your .env file contains confidential project details such as database credentials, API secrets, and other sensitive information. Pushing this to a public repository could expose your project to security vulnerabilities.
  • Why node_modules/? The node_modules folder contains all the installed dependencies, which can be easily recreated by running npm install. Including this folder in your repository would unnecessarily increase the size of your project.

Step 4: Create a src Folder for Source Code

To keep your project organised, create a folder named src at the root level. This folder will hold all your source code files and modules, enabling a clean, modular project structure. For example, your folder structure might look like this:

├── src/
│ ├── server.js # Main server entry point
│ ├── app.js # Application configuration (e.g., middleware, routes)
│ └── modules/ # Folder for individual project modules
│ ├── user/ # Example module (e.g., user)
│ │ ├── model.js # Defines user schema/model
│ │ ├── index.js # Registers the module (routes, controller, etc.)
│ │ ├── controller.js # Handles incoming requests and responses
│ │ └── service.js # Business logic and operations related to user
│ └── post/ # Another example module (e.g., post)
│ ├── model.js
│ ├── index.js
│ ├── controller.js
│ └── service.js

├── .env # Environment variables
├── .gitignore # Files/folders to ignore in version control
├── package.json # NPM dependencies and scripts
  • server.js: This file serves as the main entry point, where the server is started.
  • app.js: Handles application configuration, such as middleware, route registration, etc.
  • modules/: Contains a folder for each module of your application (e.g., user, post). Inside each module:
  • model.js: Defines the database schema or model for that module.
  • index.js: Acts as the entry point for the module, typically exporting routes or integrating the module into the app.
  • controller.js: Manages incoming HTTP requests and sends appropriate responses.
  • service.js: Contains the business logic, abstracting complex operations and interactions with the model.

Step 5: Configure the Express Application

Now, configure your Express app with necessary middewares inside app.js:

// app.ccaconst express = require("express");
const cors = require("cors");
const app = express(); // Create an Express application
// Middleware configuration
app.use(express.json()); // Parse incoming JSON requests
app.use(express.urlencoded({ extended: false })); // Parse URL-encoded data
app.use(cors()); // Enable CORS for cross-origin requests
module.exports = app; // Export the configured app

Step 6: Create the Node.js Server

In the server.js file, set up your Node.js server inside server.js:

// server.js
const http = require("http");
require("dotenv").config();
const app = require("./app"); // Import the configured Express app
const server = http.createServer(app); // Create an HTTP server using the Express app
const PORT = process.env.PORT || 3000;
// Start the server and listen on the specified port
server.listen(PORT, () => {
console.log(`Server running on port: ${PORT}`);
});

Step 7: Define Start Script in package.json

To easily start your Node.js application, define a start script in your package.json file. This will allow you to run your app using the npm start command.

In the package.json, add the following to the scripts section:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./src/server.js"
}

When you enter the command — ‘npm start’ in the terminal, the node server will be up and running on the specified port.

Step 8: Database Configuration

1. Add Database Credentials in .env

First, update your .env file with your database credentials:

# Database credentials
USERNAME=root
PASSWORD=password
DATABASE=test
HOST=localhost

2. Create a Configuration Folder

In the src folder, create a folder named configs for storing various configuration files (e.g., database configuration, Firebase configuration, etc.):

3. Define Database Configuration in db.js

Inside the configs folder, create a file called db.js to define the database configuration based on the environment (development or production):

// src/configs/db.js
require("dotenv").config(); // Load environment variables
const config = {
development: {
username: process.env.USERNAME,
password: process.env.PASSWORD,
database: process.env.DATABASE,
host: process.env.HOST,
port: 3306,
dialect: "mysql",
logging: false,
},
production: {
username: process.env.USERNAME,
password: process.env.PASSWORD,
database: process.env.DATABASE,
host: process.env.HOST,
port: 3306,
dialect: "mysql",
logging: false,
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false,
},
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
requestTimeoutMillis: 15000,
},
},
};
module.exports = config;
  • development: Configures the database connection for local development.
  • production: Configures the database connection for production, including SSL options.

5. Create a Utility Folder for Database Connection

In the src folder, create a folder named utils for storing utility components like database connections, common services (e.g., email, Firebase notifications), etc.

Inside src/utils/db.js, set up the database connection using Sequelize:

// src/utils/db.js
const Sequelize = require("sequelize");
const dbConfig = require("../configs/db.js"); // Import database configuration
// Create a Sequelize instance based on the environment
const sequelize = new Sequelize(dbConfig[process.env.NODE_ENV]);
// Establish the database connection
sequelize
.sync()
.then(() => {
console.log("✔ Database connected successfully.");
})
.catch((err) => {
console.error("✘ Error connecting to the database:", err);
});
module.exports = sequelize; // Export the Sequelize instance

You can now import the database connection from db.js wherever database access is required, such as when defining models or interacting with the database.

Step 9: Add the User Module

1. Define the User Model Schema

In the modules/user/model.js file, define the schema for the User table:

// src/modules/user/model.js
const { DataTypes } = require("sequelize");
const sequelize = require("../../utils/db"); // Import the database connection
const User = sequelize.define(
"user",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
allowNull: false,
autoIncrement: true,
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
},
gender: {
type: DataTypes.ENUM("Male", "Female", "Other"),
allowNull: false,
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
deletedAt: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
paranoid: true, // Enables soft deletes
}
);
module.exports = User;

2. Define the User Service

In the modules/user/service.js file, define methods for implementing business logic and database interactions:

// src/modules/user/service.js
const User = require("./model");
exports.createUser = (userData) => {
// Implement additional business logic if necessary
return User.create(userData);
};
exports.getUsers = (query) => {
// Add pagination
const { page = 1, limit = 10 } = query;
const offset = (page - 1) * limit;
// Filter by gender
const filter = {};
const { gender } = query;
if (gender) filter.gender = gender;
// Other business logic if required
return User.findAll({
where: filter,
limit,
offset,
order: [["name", "ASC"]],
});
};
// < define other services like update, delete, etc >
  • createUser: Handles the creation of new users.
  • getUsers: Implements pagination and filtering by gender.

3. Define the User Controller

In the modules/user/controller.js file, define functions to handle HTTP requests:

// src/modules/user/controller.js
const userService = require("./service");
exports.addUser = async (req, res, next) => {
try {
const userData = req.body;
const newUser = await userService.createUser(userData);
res.status(200).json({
status: "success",
message: "User created successfully.",
data: {
newUser,
},
});
} catch (err) {
console.error(err);
res.status(500).json({ status: "error", message: "Error creating user." });
}
};
exports.getAllUsers = async (req, res, next) => {
try {
const users = await userService.getUsers(req.query);
res.status(200).json({
status: "success",
data: {
users,
},
});
} catch (err) {
console.error(err);
res.status(500).json({ status: "error", message: "Error fetching users." });
}
};
  • addUser: Handles creating a new user and sending a response.
  • getAllUsers: Fetches users with pagination and filters.

4. Define Express Routes

In the modules/user/index.js file, define the Express router for handling various routes:

// src/modules/user/index.js
const express = require("express");
const userController = require("./controller");
const router = express.Router();
router.post("/add-new-user", userController.addUser);
router.get("/user-list", userController.getAllUsers);
module.exports = router;

Step 10: Register the User Module in app.js

Now that you have defined the User module, import and register the routes in the main app.js file:

// src/app.js
const express = require("express");
const cors = require("cors");
const userRoutes = require("./modules/user"); // Import the user routes
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cors());
app.use("/api/users", userRoutes); // Register user routes
module.exports = app;

This ensures the User module is set up with a model, service, controller, and routes, making the structure modular and easy to scale.

Now, you can call your API using endpoints like:

POST http://localhost:3000/api/users/add-new-user

This allows you to add a new user to the system by making a POST request with the required data. Similarly, you can create additional routes for other CRUD operations, such as fetching users, updating profiles, or deleting records, all using the modular structure you’ve set up.

That’s it! You now have a fully modular Node.js project structure with Express and Sequelize. Feel free to extend this by adding more modules, services, and routes as needed.

For the complete code and setup, check out the GitHub repository:
👉🏻https://github.com/Jay-Jethava/express-modular-project-structure.git

--

--

Jay Jethava

👨‍💻 As a versatile backend developer, I specialize in Node.js, creating efficient and scalable solutions with more than 3 years of experience.