Node.js Project Structure

React does not enforce a folder structure, and neither does Node.js. These are practical patterns that work well for most projects.

A minimal Node.js project

The smallest possible project is a single file:

my-app/
├── index.js
└── package.json

Run npm init -y to create package.json, then create index.js. This is enough to start.

A typical Express project

Once you add Express, routing, and static files, a common structure looks like this:

my-app/
├── src/
│   ├── routes/          # route handlers
│   │   ├── users.js
│   │   └── products.js
│   ├── controllers/     # business logic (optional, for larger apps)
│   ├── middleware/      # custom middleware functions
│   └── index.js         # entry point: sets up Express and starts server
├── public/              # static files served by Express
│   ├── index.html
│   └── style.css
├── .env                 # environment variables (never commit this)
├── .env.example         # example env vars with no real values (commit this)
├── .gitignore
├── package.json
└── package-lock.json

package.json scripts

Always include at least a start and dev script:

{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "jest"
  }
}
  • npm start: runs the app without auto-restart, used in production
  • npm run dev: runs nodemon, restarts on file changes during development
  • npm test: runs your test suite

The .gitignore file

Create a .gitignore in your project root. At minimum it should contain:

# Dependencies
node_modules/

# Environment variables (contains secrets)
.env

# Build output
dist/
build/

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

Never commit node_modules/ (it is large and reproducible from package.json) or .env (it contains secrets).

What to commit to git

Commit:

  • All source code (src/, public/)
  • package.json and package-lock.json
  • .env.example (variable names with no real values)
  • .gitignore

Do not commit:

  • node_modules/
  • .env

routes vs controllers

For small apps, put all logic directly in route handlers. For larger apps, separate the routing (what URLs exist) from the business logic (what to do when a URL is hit).

// routes/users.js: defines the URL structure
router.get('/', getUsers);
router.get('/:id', getUserById);

// controllers/users.js: defines what actually happens
export async function getUsers(req, res) {
  const users = await db.query('SELECT * FROM users');
  res.json(users);
}

This keeps routes easy to scan at a glance and controllers easy to unit test.