Best Practices for Structuring Your Backend!


When building a backend for an application, one of the most popular architectural patterns is MVC (Model-View-Controller). Let’s break it down using a simple real-world analogy – a restaurant.
How a Restaurant Works
Imagine you go to a restaurant. Here’s how things flow:
You (Customer): You look at the menu and place an order.
Waiter: Takes your order and delivers it to the chef.
Chef: Cooks the food and informs the waiter when it’s ready.
Waiter: Serves the food to you.
Each person in the restaurant has a specific responsibility. Now, let’s map this to MVC:
Model (M) → The chef (handles actual business logic – prepares the food).
View (V) → The menu & the presentation of food (what the user sees – frontend).
Controller (C) → The waiter (takes requests from customers, forwards them, and returns responses).
This structure follows the Single Responsibility Principle (SRP), ensuring each part has a clear, distinct role.
Problems with Basic MVC
While MVC is great, it has some issues when handling modern backend structures:
Over-simplification – One layer often ends up handling too many responsibilities.
Frontend in Backend – Earlier, MVC handled both frontend and backend, but modern frontend apps (React, Angular, Vue) have become heavier, making backend and frontend separation necessary.
So, how do we structure our backend better?
A Better Backend Structure
To make our backend modular and maintainable, we divide it into multiple layers:
1. Routing Layer
- Identifies incoming requests and forwards them to the right function.
// routes/userRoutes.js
const express = require('express');
const { createUser } = require('../controllers/userController');
const router = express.Router();
router.post('/users', createUser);
module.exports = router;
It acts as a map for incoming requests.
When a user makes a request to /user, it sends the request to the Controller Layer (just like a waiter receiving an order).
2. Validation Layer
- Ensures the request data is valid (e.g., using Zod for validation).
Example: Validation Layer with Zod
// validation/userValidation.js
const { z } = require('zod');
const userSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email format"),
age: z.number().int().positive("Age must be a positive integer"),
});
module.exports = { userSchema };
It checks if the request data is correct before sending it forward.
If a user provides an invalid name, email, or age, it stops the request and returns an error.
This prevents invalid data from reaching the database.
3. Controller Layer
Acts like a waiter – receives input from the routing layer and passes it to the next layer.
Returns responses from lower layers to the user.
// controllers/userController.js
const { userSchema } = require('../validation/userValidation');
const userService = require('../services/userService');
const createUser = async (req, res) => {
try {
const validatedData = userSchema.parse(req.body);
const newUser = await userService.createUser(validatedData);
res.status(201).json(newUser);
} catch (error) {
res.status(400).json({ error: error.errors });
}
};
module.exports = { createUser };
Validates the input (ensures the data is correct).
Passes the data to the Service Layer (which acts like a kitchen).
Returns a response to the user (either success or error).
4. Service Layer
- Implements business logic but does not interact with the database directly.
// services/userService.js
const userRepository = require('../repositories/userRepository');
const createUser = async (userData) => {
return await userRepository.create(userData);
};
module.exports = { createUser };
This is the kitchen where the actual cooking happens.
It receives the request from the controller and processes the data.
It does not interact with the database directly but instead passes the request to the Repository Layer (which is like a food supplier).
5. Repository Layer
Handles database interactions (CRUD operations).
Why do we need this layer? If your service layer directly interacts with the database, changing the database (e.g., from PostgreSQL to MongoDB) would require modifying core business logic. By using a repository layer, we isolate database interactions from business logic, making changes easier and structured.
// repositories/userRepository.js
const db = require('../config/db');
const create = async (userData) => {
return await db('users').insert(userData).returning('*');
};
module.exports = { create };
This layer directly communicates with the database and is responsible for storing/retrieving data.
Why do we need this layer?
If we ever change our database (from MySQL to MongoDB, for example), we only need to update this layer.
Our business logic (Service Layer) remains unchanged.
This makes our backend more flexible and easy to maintain.
6. DTO (Data Transfer Object) Layer
- The data we receive from users might be different from what we store in the database or return in the response. DTO ensures a clear structure.
// dtos/userDTO.js
const formatUserResponse = (user) => {
return {
id: user.id,
fullName: user.name,
email: user.email,
};
};
module.exports = { formatUserResponse };
If the database stores the user’s name as
"John Doe"
, we can rename it to"fullName"
in the response.This keeps frontend and backend data separate and more readable.
7. Configuration Layer
- Stores configuration values required across different layers.
// config/db.js
const knex = require('knex');
const knexConfig = require('../knexfile');
const db = knex(knexConfig.development);
module.exports = db;
This stores database settings (so we don’t have to write them everywhere in the code).
- This makes it easy to switch databases or change settings without modifying multiple files.
Final Folder Structure
backend/
│-- config/
│ ├── db.js
│
│-- controllers/
│ ├── userController.js
│
│-- repositories/
│ ├── userRepository.js
│
│-- services/
│ ├── userService.js
│
│-- validation/
│ ├── userValidation.js
│
│-- routes/
│ ├── userRoutes.js
│
│-- dtos/
│ ├── userDTO.js
│
│-- models/
│ ├── userModel.js
│
│-- migrations/
│ ├── 202403010_create_users_table.js
│
│-- utils/
│ ├── helperFunctions.js
│
│-- app.js
│-- server.js
│-- package.json
│-- knexfile.js
So, instead of a basic MVC, a layered backend architecture is the way forward for modern web applications!
Subscribe to my newsletter
Read articles from Nitesh Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
