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:

  1. Separation of Concerns: Each directory has a single responsibility

    • controllers — parse requests, call services, send responses
    • services — business logic (no HTTP knowledge)
    • models — data shape and validation
    • routes — URL-to-controller mapping
  2. Dependency direction: Routes → Controllers → Services → Models

    • Never import controllers from models
    • Services should be testable without Express
  3. Config from environment: All configuration comes from environment variables via a central config/ module — never hardcode values

  4. Separate app.js from server.js:

    • app.js creates and configures the Express app
    • server.js calls app.listen()
    • This allows tests to import app.js without 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

codeTap to expand ⛶
1// Professional Project Structure — Key Files
2
3// === src/config/index.js — Centralized configuration ===
4import dotenv from "dotenv";
5dotenv.config();
6
7const config = {
8 env: process.env.NODE_ENV || "development",
9 port: parseInt(process.env.PORT || "3000", 10),
10
11 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 },
17
18 jwt: {
19 secret: process.env.JWT_SECRET || "dev-secret-change-me",
20 expiresIn: process.env.JWT_EXPIRES_IN || "7d",
21 },
22
23 cors: {
24 origin: process.env.CORS_ORIGIN || "http://localhost:3000",
25 },
26
27 isProduction: process.env.NODE_ENV === "production",
28 isDevelopment: process.env.NODE_ENV !== "production",
29};
30
31// Validate required config in production
32if (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}
39
40export default config;
41
42// === 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";
49
50const app = express();
51
52// Security middleware
53app.use(helmet());
54app.use(cors({ origin: config.cors.origin }));
55
56// Body parsing
57app.use(express.json({ limit: "10mb" }));
58app.use(express.urlencoded({ extended: true }));
59
60// Routes
61app.use("/api", routes);
62
63// Health check
64app.get("/health", (req, res) => {
65 res.json({ status: "healthy", uptime: process.uptime() });
66});
67
68// Error handling (must be last)
69app.use(errorHandler);
70
71export default app;
72
73// === src/server.js — Entry point ===
74import app from "./app.js";
75import config from "./config/index.js";
76
77const server = app.listen(config.port, () => {
78 console.log(`Server running on port ${config.port} [${config.env}]`);
79});
80
81// Graceful shutdown
82function shutdown(signal) {
83 console.log(`${signal} received. Shutting down gracefully...`);
84 server.close(() => {
85 console.log("HTTP server closed");
86 process.exit(0);
87 });
88
89 // Force exit after 10 seconds
90 setTimeout(() => {
91 console.error("Forced shutdown after timeout");
92 process.exit(1);
93 }, 10000);
94}
95
96process.on("SIGTERM", () => shutdown("SIGTERM"));
97process.on("SIGINT", () => shutdown("SIGINT"));
98
99// === src/routes/index.js — Route aggregator ===
100import { Router } from "express";
101import userRoutes from "./userRoutes.js";
102import authRoutes from "./authRoutes.js";
103
104const router = Router();
105
106router.use("/auth", authRoutes);
107router.use("/users", userRoutes);
108
109export default router;
110
111// === 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";
115
116 // Log the full error internally
117 console.error("Error:", {
118 message: err.message,
119 stack: err.stack,
120 path: req.path,
121 method: req.method,
122 });
123
124 res.status(statusCode).json({
125 success: false,
126 error: message,
127 ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
128 });
129}

🏋️ Practice Exercise

Exercises:

  1. Scaffold a complete Node.js project structure using the pattern above — create all directories and placeholder files
  2. Implement the config module that validates required env vars and throws on missing ones in production
  3. Create the app.js / server.js separation and write a test that imports app.js without starting a server
  4. Build a route aggregator that loads route files dynamically from a routes/ directory
  5. Implement a graceful shutdown handler that closes database connections, HTTP server, and background jobs
  6. Create an .env.example file 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, or next references

  • Not separating app.js from server.js — this makes integration testing impossible because importing the app starts the server

  • Hardcoding 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.