Why Professional Setup Matters
The way you structure a Node.js project from day one directly affects how easy it is to maintain, scale, and collaborate on as the project grows. A professional folder structure, proper tooling configuration, and clean separation of concerns are what distinguish a junior developer's side project from a production-grade backend.
---
Professional Folder Structure
my-backend/
├── src/
│ ├── controllers/ # Request handlers (business logic)
│ ├── routes/ # Express Router definitions
│ ├── models/ # Mongoose schemas and models
│ ├── middlewares/ # Custom Express middlewares
│ ├── utils/ # Reusable utility functions
│ ├── config/ # Configuration files (db, cloudinary, etc.)
│ ├── constants/ # App-wide constants and enums
│ └── types/ # TypeScript type declarations (if using TS)
├── public/ # Temp file storage (multer uploads)
├── .env # Environment variables (not committed)
├── .env.example # Template for env vars (committed)
├── .gitignore
├── .eslintrc.json
├── .prettierrc
├── nodemon.json
├── package.json
├── tsconfig.json # (if using TypeScript)
├── index.js # Entry point — starts the server
└── src/app.js # Express app configuration (no server.listen here)
---
Why Separate app.js from index.js
This is a common industry pattern with clear benefits:
src/app.js — Express application setup:
- Creates
express()instance - Registers all global middleware (cors, json parser, cookie-parser, morgan)
- Mounts all routers (
app.use('/api/users', userRouter)) - Does NOT call
app.listen() - Exported as
export default app - Easy to import in tests without starting the server
index.js (root level) — Server entry point:
- Imports the
appfromsrc/app.js - Connects to the database
- Only after successful DB connection, starts the server with
app.listen() - Handles top-level
async/awaiterrors and process exit
This separation means tests can import the app without actually starting the server and binding to a port.
---
package.json Scripts
{
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"build": "tsc",
"start:prod": "node dist/index.js",
"lint": "eslint src --ext .js,.ts",
"lint:fix": "eslint src --ext .js,.ts --fix",
"format": "prettier --write src/**/*.{js,ts}"
}
}
---
nodemon Configuration
nodemon.json controls nodemon's behaviour:
{
"watch": ["src"],
"ext": "js,ts,json",
"ignore": ["src/**/*.test.js", "node_modules"],
"exec": "node src/index.js"
}
With TypeScript: use ts-node instead: "exec": "ts-node src/index.ts"
---
ESLint + Prettier Setup
ESLint catches code errors and enforces consistent code style. Prettier formats code automatically.
npm install -D eslint prettier eslint-config-prettier eslint-plugin-prettier
npx eslint --init
``.eslintrc.json`` example:
{
"env": { "node": true, "es2021": true },
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"rules": {
"no-console": "warn",
"no-unused-vars": "error"
}
}
``.prettierrc`` example:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
---
dotenv Setup
// src/config/config.js — centralised config object
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: process.env.PORT || 5000,
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET,
jwtExpiry: process.env.JWT_EXPIRY || '15m',
jwtRefreshExpiry: process.env.JWT_REFRESH_EXPIRY || '7d',
nodeEnv: process.env.NODE_ENV || 'development',
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:5173',
cloudinary: {
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
apiKey: process.env.CLOUDINARY_API_KEY,
apiSecret: process.env.CLOUDINARY_API_SECRET,
},
};
Always access config through this object, not directly through process.env. This makes it easy to validate, mock in tests, and see all config in one place.
---
.gitignore for Node.js
node_modules/
.env
.env.local
dist/
build/
*.log
public/temp/*
.DS_Store
coverage/
---
cross-env for Windows/Mac Compatibility
Some npm scripts use environment variables inline: NODE_ENV=production node index.js. This syntax does not work on Windows Command Prompt. Use cross-env:
npm install -D cross-env
"scripts": {
"start:prod": "cross-env NODE_ENV=production node src/index.js"
}
---
Module Exports Pattern
Use named exports for models, utilities, controllers, and middleware. Use default export only for the Express app and database connection function. This makes imports clear and explicit:
// ❌ Avoid: default export for everything
export default router;
// ✅ Prefer: named exports
export { userRouter, authRouter };
export { UserModel };
export { asyncHandler, ApiError, ApiResponse };
Createsrc/index.jsbarrel files inside each directory to re-export everything:export { UserModel } from './User.model.js'. This allows clean imports elsewhere:import { UserModel } from '../models'.