Professional Project Structure
📖 Concept
A well-organized project structure is the foundation of maintainable, scalable Node.js applications. There's no single "correct" structure, but established patterns make it easy for teams to navigate and contribute.
Recommended project structure for an API server:
my-node-app/
├── src/
│ ├── config/ # Configuration files
│ │ ├── index.js # Central config (reads env vars)
│ │ ├── database.js # Database connection config
│ │ └── logger.js # Logger setup
│ ├── controllers/ # Request handlers (thin layer)
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/ # Express middleware
│ │ ├── auth.js # Authentication middleware
│ │ ├── errorHandler.js
│ │ └── validate.js # Request validation
│ ├── models/ # Data models / schemas
│ │ ├── User.js
│ │ └── Post.js
│ ├── routes/ # Route definitions
│ │ ├── index.js # Route aggregator
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── services/ # Business logic
│ │ ├── authService.js
│ │ └── userService.js
│ ├── utils/ # Shared utilities
│ │ ├── errors.js # Custom error classes
│ │ └── helpers.js
│ ├── app.js # Express app setup (no listen)
│ └── server.js # Entry point (calls app.listen)
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── scripts/ # Build, migration, seed scripts
├── docs/ # API documentation
├── .env.example # Environment variable template
├── .gitignore
├── .eslintrc.json
├── package.json
└── README.md
Key architectural principles:
Separation of Concerns: Each directory has a single responsibility
controllers— parse requests, call services, send responsesservices— business logic (no HTTP knowledge)models— data shape and validationroutes— URL-to-controller mapping
Dependency direction: Routes → Controllers → Services → Models
- Never import controllers from models
- Services should be testable without Express
Config from environment: All configuration comes from environment variables via a central
config/module — never hardcode valuesSeparate
app.jsfromserver.js:app.jscreates and configures the Express appserver.jscallsapp.listen()- This allows tests to import
app.jswithout starting a server
🏠 Real-world analogy: A project structure is like a well-organized office building — reception (routes) directs visitors, managers (controllers) coordinate, specialists (services) do the work, and filing cabinets (models) store the data. Everyone knows where to find things.
💻 Code Example
1// Professional Project Structure — Key Files23// === src/config/index.js — Centralized configuration ===4import dotenv from "dotenv";5dotenv.config();67const config = {8 env: process.env.NODE_ENV || "development",9 port: parseInt(process.env.PORT || "3000", 10),1011 db: {12 uri: process.env.DATABASE_URL || "mongodb://localhost:27017/myapp",13 options: {14 maxPoolSize: parseInt(process.env.DB_POOL_SIZE || "10", 10),15 },16 },1718 jwt: {19 secret: process.env.JWT_SECRET || "dev-secret-change-me",20 expiresIn: process.env.JWT_EXPIRES_IN || "7d",21 },2223 cors: {24 origin: process.env.CORS_ORIGIN || "http://localhost:3000",25 },2627 isProduction: process.env.NODE_ENV === "production",28 isDevelopment: process.env.NODE_ENV !== "production",29};3031// Validate required config in production32if (config.isProduction) {33 const required = ["DATABASE_URL", "JWT_SECRET"];34 const missing = required.filter((key) => !process.env[key]);35 if (missing.length > 0) {36 throw new Error(`Missing required env vars: ${missing.join(", ")}`);37 }38}3940export default config;4142// === src/app.js — Express app setup (no server.listen!) ===43import express from "express";44import helmet from "helmet";45import cors from "cors";46import config from "./config/index.js";47import routes from "./routes/index.js";48import { errorHandler } from "./middleware/errorHandler.js";4950const app = express();5152// Security middleware53app.use(helmet());54app.use(cors({ origin: config.cors.origin }));5556// Body parsing57app.use(express.json({ limit: "10mb" }));58app.use(express.urlencoded({ extended: true }));5960// Routes61app.use("/api", routes);6263// Health check64app.get("/health", (req, res) => {65 res.json({ status: "healthy", uptime: process.uptime() });66});6768// Error handling (must be last)69app.use(errorHandler);7071export default app;7273// === src/server.js — Entry point ===74import app from "./app.js";75import config from "./config/index.js";7677const server = app.listen(config.port, () => {78 console.log(`Server running on port ${config.port} [${config.env}]`);79});8081// Graceful shutdown82function shutdown(signal) {83 console.log(`${signal} received. Shutting down gracefully...`);84 server.close(() => {85 console.log("HTTP server closed");86 process.exit(0);87 });8889 // Force exit after 10 seconds90 setTimeout(() => {91 console.error("Forced shutdown after timeout");92 process.exit(1);93 }, 10000);94}9596process.on("SIGTERM", () => shutdown("SIGTERM"));97process.on("SIGINT", () => shutdown("SIGINT"));9899// === src/routes/index.js — Route aggregator ===100import { Router } from "express";101import userRoutes from "./userRoutes.js";102import authRoutes from "./authRoutes.js";103104const router = Router();105106router.use("/auth", authRoutes);107router.use("/users", userRoutes);108109export default router;110111// === src/middleware/errorHandler.js ===112export function errorHandler(err, req, res, next) {113 const statusCode = err.statusCode || 500;114 const message = err.isOperational ? err.message : "Internal server error";115116 // Log the full error internally117 console.error("Error:", {118 message: err.message,119 stack: err.stack,120 path: req.path,121 method: req.method,122 });123124 res.status(statusCode).json({125 success: false,126 error: message,127 ...(process.env.NODE_ENV === "development" && { stack: err.stack }),128 });129}
🏋️ Practice Exercise
Exercises:
- Scaffold a complete Node.js project structure using the pattern above — create all directories and placeholder files
- Implement the config module that validates required env vars and throws on missing ones in production
- Create the app.js / server.js separation and write a test that imports app.js without starting a server
- Build a route aggregator that loads route files dynamically from a
routes/directory - Implement a graceful shutdown handler that closes database connections, HTTP server, and background jobs
- Create an
.env.examplefile documenting all required and optional environment variables
⚠️ Common Mistakes
Putting everything in a single file — even a small project benefits from separating routes, controllers, and services
Mixing Express-specific code into service files — services should contain pure business logic with no
req,res, ornextreferencesNot separating
app.jsfromserver.js— this makes integration testing impossible because importing the app starts the serverHardcoding configuration values — ports, database URLs, and secrets must come from environment variables for proper deployment
Not implementing graceful shutdown — abrupt process termination drops active connections and can corrupt in-flight database operations
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Professional Project Structure. Login to unlock this feature.