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
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
// 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
// middleware/notFound.js
function notFound(req, res) {
res.status(404).json({ error: 'NotFound', message: `No route for ${req.method} ${req.path}` });
}
module.exports = notFound;
// 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;
// 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
// 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`));
curl http://localhost:4602/tasks
[{"id":1,"title":"Write lesson","done":false},{"id":2,"title":"Verify code","done":true}]
curl -w " [%{http_code}]" http://localhost:4602/nope
{"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
- Build the
config/,middleware/, andutils/folders above, and wire them into yourapp.jsfrom Lesson 4. - Add a second resource (
users,products, or similar) following the exact same model/controller/routes pattern, and mount it inapp.jsalongsidetasks. - Confirm both resources work independently with
curl, and that adding the second one didn’t require changing any file related to the first. - Explain, in your own words, why centralizing configuration in
config/index.jsis preferable to readingprocess.envdirectly in whichever file happens to need a setting.
Recap
config/,middleware/, andutils/give reusable, cross-cutting code (settings, error handling, shared helpers) a consistent, predictable home.- A well-structured
app.jsreads 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.