CodingNic

Project Structure & MVC

Structuring a Growing Express App

Project Structure & MVC 12 min read

Structuring a Growing Express App

Objectives

By the end of this lesson, you should be able to:

  • Organize configuration, middleware, and error handling into their own folders
  • Add a second resource to an MVC app without disrupting the first
  • Recognize the folder conventions used across most real Express projects

๐Ÿ’ก Why this matters: A single-resource MVC app (Lessons 2 through 4) is a clean starting point, a real app has multiple resources, shared configuration, and custom middleware, all needing a consistent place to live.

โš ๏ธ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests, across the complete real, multi-file project.

Project File Structure

text
project/
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ app.js
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ index.js
โ”œโ”€โ”€ middleware/
โ”‚   โ”œโ”€โ”€ errorHandler.js
โ”‚   โ””โ”€โ”€ notFound.js
โ”œโ”€โ”€ utils/
โ”‚   โ””โ”€โ”€ AppError.js
โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ taskModel.js
โ”œโ”€โ”€ controllers/
โ”‚   โ””โ”€โ”€ taskController.js
โ””โ”€โ”€ routes/
    โ””โ”€โ”€ taskRoutes.js

Three new folders, on top of Lesson 4’s structure: config/ for environment-driven settings, middleware/ for reusable request-handling functions (Module 7), and utils/ for shared helper code, here, the AppError class from Module 8.

Configuration

javascript
// config/index.js
module.exports = {
  port: process.env.PORT || 4602,
  env: process.env.NODE_ENV || 'development'
};

Centralizing environment variables (Module 3’s process.env) in one file, rather than scattering process.env.PORT calls across the codebase, means every default value and every environment-driven setting lives in exactly one place, easy to find, easy to change.

Middleware and Utilities in Their Own Files

javascript
// middleware/notFound.js
function notFound(req, res) {
  res.status(404).json({ error: 'NotFound', message: `No route for ${req.method} ${req.path}` });
}

module.exports = notFound;
javascript
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  const statusCode = err.statusCode || 500;
  console.error(`[${err.name || 'Error'}] ${err.message}`);
  res.status(statusCode).json({
    error: err.name || 'InternalServerError',
    message: err.isOperational ? err.message : 'Something went wrong'
  });
}

module.exports = errorHandler;
javascript
// utils/AppError.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

module.exports = AppError;

Every piece of Module 7 and Module 8’s error-handling work, the 404 handler, the global error handler, the custom error class, is exactly the kind of code that belongs in its own file rather than inline in app.js, each one is a single, focused, exportable piece.

Assembling the Full App

javascript
// app.js
const express = require('express');
const config = require('./config');
const taskRoutes = require('./routes/taskRoutes');
const notFound = require('./middleware/notFound');
const errorHandler = require('./middleware/errorHandler');

const app = express();
app.use(express.json());
app.use('/tasks', taskRoutes);

app.use(notFound);
app.use(errorHandler);

app.listen(config.port, () => console.log(`listening on ${config.port} in ${config.env} mode`));
bash
curl http://localhost:4602/tasks
text
[{"id":1,"title":"Write lesson","done":false},{"id":2,"title":"Verify code","done":true}]
bash
curl -w " [%{http_code}]" http://localhost:4602/nope
text
{"error":"NotFound","message":"No route for GET /nope"} [404]

app.js reads almost like a table of contents now, config, routes, error handling, each imported from its own clearly-named file, rather than containing any of that logic directly. This is the real payoff of the whole module: app.js for a project with ten resources looks almost identical to this, ten more app.use('/resource', resourceRoutes) lines, not ten times the complexity crammed into one file.

Adding a Second Resource

Adding a users resource means repeating Lessons 2 through 4’s pattern, models/userModel.js, controllers/userController.js, routes/userRoutes.js, then one more line in app.js, app.use('/users', userRoutes), nothing about the existing tasks resource needs to change at all, each resource is fully self-contained.

Try It

  1. Build the config/, middleware/, and utils/ folders above, and wire them into your app.js from Lesson 4.
  2. Add a second resource (users, products, or similar) following the exact same model/controller/routes pattern, and mount it in app.js alongside tasks.
  3. Confirm both resources work independently with curl, and that adding the second one didn’t require changing any file related to the first.
  4. Explain, in your own words, why centralizing configuration in config/index.js is preferable to reading process.env directly in whichever file happens to need a setting.

Recap

  • config/, middleware/, and utils/ give reusable, cross-cutting code (settings, error handling, shared helpers) a consistent, predictable home.
  • A well-structured app.js reads like a table of contents, importing and wiring together pieces defined elsewhere, containing very little logic itself.
  • Adding a new resource means repeating the same model/controller/routes pattern and adding one line to app.js, existing resources stay completely untouched.

Next lesson: when to split further, a preview of service and repository layers.