Node.js MVC Architecture in Express.js: Models Controllers and Routes Episode 31 (Updated June 2026)
TCS laid off 12,000 engineers in July 2025 — and the pattern is consistent across the industry. Routine-task engineers get replaced. Engineers who build organized, scalable backend applications get hired at double the salary. The difference between the two is often not language knowledge — it is architecture. MVC (Model-View-Controller) is the architecture pattern that every professional Node.js project uses, and the engineers who write all their logic in a single app.js file are the ones who get flagged as junior in technical reviews and struggle to grow. Episode 31 of our Node.js backend series breaks down MVC in Express.js completely — how to set up the folder structure, what belongs in Models, Controllers and Routes, and how to integrate file upload cleanly within the MVC pattern.
- MVC separates your Express app into Model (data logic), View (response format) and Controller (request handling)
- Controllers hold the business logic — route files only call controller functions, nothing else
- Models define the schema and data methods — controllers call models, never databases directly
- Multer file upload middleware plugs into MVC cleanly as a controller-level middleware import
- MVC architecture is required in all professional Node.js codebases at TCS, Infosys and product companies
What MVC Architecture Is and Why Express.js Needs It
Without MVC, an Express.js application grows into a single sprawling file — routes mixed with database calls mixed with validation mixed with response logic. This is called spaghetti code, and it kills maintainability. When a new developer joins the project (or when you come back to your own code after 3 months), nobody can find where things live. MVC solves this by enforcing a strict separation: the Model handles all data and database interaction, the View handles what the client receives (in API applications, this is usually JSON), and the Controller handles the incoming request, calls the Model for data, and sends the response. Each piece has one job. When something breaks, you know exactly which file to look in. When a feature needs adding, you know exactly where the new code belongs.

The MVC Folder Structure for an Express.js Project
A standard Express.js MVC project has this folder structure: project-root/ with server.js (or app.js) at the top level, and subfolders for models/ (containing User.js, Post.js, etc.), controllers/ (containing authController.js, userController.js), routes/ (containing authRoutes.js, userRoutes.js), middlewares/ (containing authMiddleware.js, uploadMiddleware.js), config/ (containing db.js for database connection and any config files), and public/ and views/ if serving HTML. The key rule: routes/ files contain only path definitions and point to controller functions. No logic in routes. All database calls happen in models/ or within controllers via model methods. Middleware goes in middlewares/ and is imported only where needed.
| MVC Layer | File Location | What It Contains | What It Must NOT Contain |
|---|---|---|---|
| Model | models/User.js | Schema, DB methods, validations | req / res / route logic |
| Controller | controllers/userController.js | Business logic, model calls, responses | Direct DB queries, route paths |
| Route | routes/userRoutes.js | Path definitions, middleware chain | Business logic, DB calls |
| Middleware | middlewares/upload.js | Auth, upload, validation logic | Route paths, response logic |
| Config | config/db.js | DB connection, environment vars | Any request handling |
Building the Model: Mongoose Schema and Database Methods
Create a User model in models/User.js. With Mongoose: const mongoose = require("mongoose"); const userSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: String, profilePic: String, createdAt: { type: Date, default: Date.now } }). const User = mongoose.model("User", userSchema); module.exports = User. The model file only defines schema and exports the model. No route handling, no request/response logic. If you need custom methods (like finding a user by email with password excluded), add them as schema methods or statics directly on the model: userSchema.statics.findByEmail = function(email) { return this.findOne({ email }).select("-password"); }.

Building the Controller: Request Handling Without Route Pollution
Create a controller in controllers/userController.js. A controller function receives req and res and handles one specific action: const User = require("../models/User"); exports.getUserProfile = async function(req, res) { try { const user = await User.findById(req.params.id).select("-password"); if (!user) return res.status(404).json({ error: "User not found" }); res.json(user); } catch (err) { res.status(500).json({ error: err.message }); } }. Notice: the controller imports the Model and calls it. It does not write mongoose queries inline — the Model handles data access. It does not define the route path — the Router does that. The controller just receives the request, calls the right model method, and sends the response. This is the core MVC contract.
Setting Up Routes to Call Controllers
Create routes in routes/userRoutes.js: const express = require("express"); const router = express.Router(); const userController = require("../controllers/userController"); router.get("/:id", userController.getUserProfile); router.put("/:id", userController.updateUserProfile); router.delete("/:id", userController.deleteUser); module.exports = router. In server.js, mount the router: app.use("/api/users", userRoutes). The route file reads like a table of contents: GET /api/users/:id calls getUserProfile, PUT /api/users/:id calls updateUserProfile. No logic here — just mapping paths to controller functions. This makes the API contract immediately readable without needing to read through implementation code.
Integrating Multer File Upload Inside the MVC Pattern
Multer integrates cleanly into MVC as a middleware exported from middlewares/uploadMiddleware.js: const multer = require("multer"); const storage = multer.diskStorage({ destination: function(req, file, cb) { cb(null, "uploads/") }, filename: function(req, file, cb) { cb(null, Date.now() + "-" + file.originalname) } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: function(req, file, cb) { if (file.mimetype.startsWith("image/")) cb(null, true); else cb(new Error("Images only"), false); } }); module.exports = upload. In the route file, import and apply the middleware: const upload = require("../middlewares/uploadMiddleware"); router.post("/:id/avatar", upload.single("avatar"), userController.uploadAvatar). The upload middleware sits between the route and the controller — the controller receives req.file already processed, with no knowledge of how storage works. Clean separation preserved.
Full Stack Development Training at ABC Trainings: Pune, Sambhajinagar, Sangli
ABC Trainings delivers Node.js and Full Stack Web Development training at five Maharashtra centers. Wagholi, Pune: 1st Floor, Laxmi Datta Arcade, Pune-Ahilyanagar Highway. Hadapsar, Pune: 1st Floor, Shree Tower, opposite Vaibhav Theater, Magarpatta. Cidco, Sambhajinagar: Kalpana Plaza, N-1 Cidco, opposite Eiffel Tower. Osmanpura, Sambhajinagar: S.S.C Board to Peer Bazar Road, near Jama Masjid. Sangli: Shubham Emphoria, 1st Floor, Above US Polo, Sangli-Miraj Road, Vishrambag. Our Full Stack curriculum covers HTML, CSS, JavaScript, Node.js, Express.js, MVC architecture, MongoDB with Mongoose, REST API design, authentication, file upload and deployment. Students build 3 to 4 real projects during the course. Placement support with mock interviews and direct company contacts included. Weekend batches available. Call 7039169629 or WhatsApp 7774002496.
Get the AI Powered Application Development Brochure + Fees + Batch Dates on WhatsApp
Free 1:1 counselling. Placement track record. CMYKPY/PMKVY eligibility check.
💬 Get Brochure on WhatsApp📞 Call 7039169629About the author: Rahul Patil. 12 yrs experience training engineers across Maharashtra.
Visit Our Centers
- Wagholi (Pune): 1st Floor, Laxmi Datta Arcade, Pune-Ahilyanagar Highway. Call 7039169629
- Hadapsar (Pune HQ): 1st Floor, Shree Tower, opp. Vaibhav Theater, Magarpatta. Call 7039169629
- Cidco (Chh. Sambhajinagar): Kalpana Plaza, opp. Eiffel Tower, N-1 Cidco. Call 7039169629
- Osmanpura (Chh. Sambhajinagar): S.S.C Board to Peer Bazar Road, near Jama Masjid. Call 7039169629
- Sangli: Shubham Emphoria, 1st Floor, Above US Polo Assn., Sangli-Miraj Rd, Vishrambag. Weekend batches available. Call 7039169629
FAQs
Why should I use MVC architecture instead of putting everything in app.js?
Putting all logic in app.js or in route files works for tutorial projects but fails at production scale. The problem is maintainability: a single file with 500-plus lines containing database calls, validation, business logic and route paths becomes impossible to debug, test or hand off to another developer. MVC fixes this by giving every type of code a designated home — so a bug in data retrieval sends you to the Model, a bug in request handling sends you to the Controller, and a missing route sends you to the Route file. Teams can also work in parallel because different developers work on different layers without conflicting with each other in the same file.
Does MVC architecture work with MongoDB and Mongoose in Node.js?
Yes. Mongoose integrates naturally with the Model layer in MVC. The Model file defines the Mongoose schema, exports the model, and optionally defines schema-level methods and statics for common queries. Controllers import the model and call its methods. For example: in models/User.js you define the schema; in controllers/authController.js you call User.findOne({ email }) or User.create({ name, email, password }). The controller never writes a raw mongoose query — it always goes through the model layer. This keeps database logic in one place and makes it easy to switch from MongoDB to a SQL database later without rewriting all your controllers.
How does Multer file upload fit inside an MVC Express.js project?
Multer file upload belongs in the Middleware layer. Create a dedicated file at middlewares/uploadMiddleware.js that exports the configured Multer instance. In your route file, import the middleware and apply it to the specific route that handles file uploads: router.post("/avatar", upload.single("avatar"), userController.uploadAvatar). The Multer middleware runs before the controller function, processes the file, and adds req.file to the request object. Your controller function then reads req.file.filename or req.file.path and saves it to the database via the User model. The file storage logic stays in the middleware layer — the controller just works with the result.
Is MVC architecture tested in Node.js developer interviews at TCS and product companies?
Yes. Code architecture is a core topic in Node.js backend developer interviews at TCS Digital, Infosys InStep, Wipro Elite and most product company backend rounds. You will typically be asked to explain what MVC is and why it is used, then either walk through your existing project architecture or write a small Express app with proper MVC separation on a whiteboard or code editor. Candidates who demonstrate clean folder structure, controller separation and no direct DB calls in routes score significantly higher than candidates who can only write working code in a single file. ABC Trainings backend module includes an architecture review session where students refactor their own projects into proper MVC structure.



