Node.js Project Structure
Written By: Avinash Malhotra
Updated on
This guide explains a recommended Node.js project structure for Express and Mongoose applications, with clear separation between routes, controllers, models, views, and configuration.
A well-organized folder layout improves maintainability, makes onboarding easier, and helps teams build scalable backend applications faster.
Why structure matters
A consistent Node.js application architecture makes it easier to read code, test components, and extend functionality as the application grows.
- Separate concerns between routing, business logic, and data models
- Keep configuration and environment setup centralized
- Make testing and debugging simpler across folders
- Support both API routes and template views cleanly
Project Setup
To set up a Node.js project, follow these steps:
- Initialize the Project: Create a new directory for your project and navigate into it. Run
npm init -yto create a package.json file with default settings. - Install Dependencies: Install necessary packages such as Express for web framework, Mongoose for MongoDB interaction, and any other libraries you need using npm.
- Organize Directory Structure: Create a logical directory structure to separate concerns. Common directories include
src/controllersfor business logic,src/modelsfor Mongoose schemas,src/routesfor API endpoints,src/viewsfor templates, andsrc/configfor environment-based configuration. - Configure Environment Variables: Use a
.envfile to store sensitive information like database connection strings and API keys. Load these variables in your application with thedotenvpackage and avoid committing secrets to source control. - Set Up Database Connection: Establish a connection to your MongoDB database using Mongoose. Create a separate configuration file such as
src/config/database.jsand import it from your main application entry point likesrc/app.jsorsrc/index.js.
File and Folder Structure
A well-structured Node.js project enhances maintainability and scalability. Here's a common directory structure:
node-app/
├── node_modules/ # Installed dependencies
├── src/ # Source code
│ ├── controllers/ # Business logic and request handling
│ ├── models/ # Mongoose schemas and models
│ ├── routes/ # API route definitions
│ ├── views/ # Template files (e.g. Nunjucks)
│ ├── config/ # Configuration and environment setup
│ ├── middlewares/ # Request middleware and auth checks
│ ├── services/ # Reusable business-services and helpers
│ ├── utils/ # Utility functions and shared helpers
│ ├── app.js # Main application file
├── public/ # Static assets (css, js, images)
├── .env # Environment variables
├── package.json # Project metadata and dependencies
├── package-lock.json # Exact versions of installed dependencies
└── README.md # Project documentation and usage notes
This structure separates different concerns of the application, making it easier to manage and scale as the project grows. The src directory contains all the source code, while the root directory holds configuration files and documentation.
Models
The Model folder contains the Mongoose models that represent the structure of your MongoDB collections.
Car Model
/*models/car.js - Mongoose model example*/
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const CarSchema = new Schema({
_id: mongoose.ObjectId,
name: { type: String, required: true },
type: { type: String, required: true },
price: { type: Number, required: true },
date: { type: Date, default: Date.now }
}, {collection: 'cars'});
export default mongoose.model("Car", CarSchema);
Admin model
Admin models can be created similarly to the Car model, with their own schemas and validation rules.
/*models/admin.js - Mongoose model example*/
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const AdminSchema = new Schema({
_id: mongoose.ObjectId,
username: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true }
}, {collection: 'admins'});
export default mongoose.model("Admin", AdminSchema);
Routes
The Routes folder contains the Express route definitions that handle incoming HTTP requests and map them to controller functions.
Car Routes
/*routes/carRoutes.js - Express routes example*/
import express from 'express';
import CarController from '../controllers/carController.js';
const router = express.Router();
router.get('/cars', CarController.getAllCars);
router.get('/cars/:id', CarController.getCarById);
router.post('/cars', CarController.createCar);
router.put('/cars/:id', CarController.updateCar);
router.delete('/cars/:id', CarController.deleteCar);
export default router;
Controller Functions
The Controllers folder contains the business logic and functions that handle the requests defined in the routes.
Controller functions are responsible for processing the incoming requests, interacting with the models, and returning appropriate responses. For example, a controller function might retrieve all cars from the database, update a car's information, or delete a car.
Car Controller
/*controllers/carController.js - Controller functions example*/
import Car from '../models/car.js';
export default {
getAllCars: async (req, res) => {
try {
const cars = await Car.find();
res.json(cars);
} catch (error) {
res.status(500).json({ error: 'Failed to retrieve cars' });
}
},
getCarById: async (req, res) => {
try {
const car = await Car.findById(req.params.id);
if (!car) {
return res.status(404).json({ error: 'Car not found' });
}
res.json(car);
} catch (error) {
res.status(500).json({ error: 'Failed to retrieve car' });
}
},
createCar: async (req, res) => {
try {
const newCar = new Car(req.body);
const savedCar = await newCar.save();
res.status(201).json(savedCar);
} catch (error) {
res.status(500).json({ error: 'Failed to create car' });
}
},
updateCar: async (req, res) => {
try {
const updatedCar = await Car.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!updatedCar) {
return res.status(404).json({ error: 'Car not found' });
}
res.json(updatedCar);
} catch (error) {
res.status(500).json({ error: 'Failed to update car' });
}
},
deleteCar: async (req, res) => {
try {
const deletedCar = await Car.findByIdAndDelete(req.params.id);
if (!deletedCar) {
return res.status(404).json({ error: 'Car not found' });
}
res.json({ message: 'Car deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete car' });
}
}
};
Views
The Views folder contains the template files that define the structure of the HTML pages rendered by the application. Common templating engines include Nunjucks, EJS, Pug, and Handlebars.
For nunjucks, use separate folders for views, css, js, and images.
node-app/
├── src/
│ ├── views/ # Nunjucks template files
│ │ ├── index.njk # Example template file
│ │ ├── cars.njk # Example template file
│ │ ├── car.njk # Example template file
│ │ ├── contact.njk # Example template file
│ ├── public/ # Static assets
│ │ ├── css/ # CSS files
│ │ ├── js/ # JavaScript files
│ │ └── images/ # Image files
Best Practices
Follow these best practices to keep your Node.js application maintainable, testable, and secure.
- Keep route definitions thin and delegate work to controller methods.
- Use a dedicated
configfolder for environment settings and database setup. - Store reusable logic in
servicesorutils, not directly in controllers. - Use centralized error handling and consistent JSON responses for API endpoints.
- Use descriptive filenames and keep the root directory clean with minimal top-level files.