Back to Blog
Lesson 29 of the Node.js: Build Your First Server & CLI course
Node.jsAugust 16, 20264 min read

Professional Project Structure: Organizing Your Express API

Stop building spaghetti code. Learn to organize your Node.js project structure for maintainability by separating routes, controllers, and models.

node.jsexpressarchitecturebest-practicesbackend
A detailed project timeline featuring design and development phases on a whiteboard with sticky notes.

Previously in this course, we explored advanced error handling to keep our API robust. In this lesson, we shift our focus from "making it work" to "making it maintainable" by refactoring our growing codebase into a professional project structure.

As your API grows, keeping everything in a single index.js file leads to "spaghetti code"—where database logic, request routing, and business rules are tangled together. This makes testing and debugging a nightmare. Today, we’ll adopt a clean architecture approach to organize your files, improve maintainability, and simplify future refactoring.

Why Project Structure Matters

In a small script, a single file is fine. In a production API, you need to know exactly where to look when a bug occurs. If your database schema is in the same file as your route handlers, changing a field name requires jumping through hundreds of lines of code.

By enforcing a clear architecture, we ensure that each file has a single responsibility. This is the bedrock of long-term project health, much like organizing test suites or modularizing React components keeps those projects manageable.

The Standard Express Directory Layout

From above of anonymous person selecting paper maps with captions in stack while spending time in library with blurred background

We will separate our code into four main layers:

  1. config/: Holds environment settings and database connection logic.
  2. models/: Defines our data structures (Mongoose schemas).
  3. controllers/: Contains the "business logic"—what happens when a request hits an endpoint.
  4. routes/: Defines the API endpoints and maps them to controllers.

The Refactored Structure

TEXT
my-api/
├── config/
│   └── db.js
├── controllers/
│   └── userController.js
├── models/
│   └── User.js
├── routes/
│   └── userRoutes.js
├── index.js
└── package.json

Worked Example: Refactoring a User Endpoint

Let's take a hypothetical "Create User" feature. Previously, you might have had the Mongoose model definition, the route definition, and the save() logic all in index.js.

1. The Model (models/User.js)

Keep data definitions isolated.

JAVASCRIPT
const mongoose = require(CE9178">'mongoose');
const userSchema = new mongoose.Schema({ name: String });
module.exports = mongoose.model(CE9178">'User', userSchema);

2. The Controller (controllers/userController.js)

The controller should only care about extracting data from the request and sending a response.

JAVASCRIPT
const User = require(CE9178">'../models/User');

exports.createUser = async (req, res) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

3. The Route (routes/userRoutes.js)

The route file acts as a map, linking the URL path to the controller function.

JAVASCRIPT
const express = require(CE9178">'express');
const router = express.Router();
const { createUser } = require(CE9178">'../controllers/userController');

router.post(CE9178">'/', createUser);
module.exports = router;

4. The Entry Point (index.js)

Finally, your index.js becomes a clean orchestration layer.

JAVASCRIPT
const express = require(CE9178">'express');
const userRoutes = require(CE9178">'./routes/userRoutes');
const app = express();

app.use(express.json());
app.use(CE9178">'/api/users', userRoutes);

app.listen(3000, () => console.log(CE9178">'Server running on port 3000'));

Hands-on Exercise

  1. Create the folders: mkdir config controllers models routes.
  2. Move your existing Mongoose schema into models/.
  3. Extract your route handler logic into a function inside controllers/.
  4. Import your new route file into index.js using app.use().
  5. Start your server and verify that your endpoints still return the correct data.

Common Pitfalls

Close-up of a triangular warning sign indicating a slippery surface, fixed to a wooden post.

  • Circular Dependencies: If userController.js imports routes/userRoutes.js and vice versa, Node.js will throw an error. Keep your dependencies flowing one way: Routes -> Controllers -> Models.
  • "Fat" Controllers: If your controller has 100+ lines of code, you're doing too much. Extract complex logic into a services/ folder.
  • Hardcoding Config: Never put database URLs directly in index.js. Use the config/ folder to manage these, which we'll expand on when we discuss environment variables.

FAQ

Q: Should I use a services/ folder? A: Yes, if your business logic grows beyond simple database calls. Controllers should handle request/response; services should handle the heavy lifting.

Q: Is this "Clean Architecture"? A: It's a simplified version. True Clean Architecture adds even more layers (Entities, Use Cases), but for a beginner Node.js API, this MVC-inspired split is the industry standard.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

By separating routes, controllers, and models, you've moved from a monolithic script to a professional project structure. This modularity makes your code easier to read, test, and scale as your requirements evolve. Remember: good architecture is about making it easy to change your mind later.

Up next: We'll dive into Environment Variables to keep your sensitive data (like database connection strings) out of your codebase.

Similar Posts