3 BACKEND API

Clean Architecture

🏛️ Clean Architecture: Building for 100,000 Lines of Code

If you put your Database Queries, your Business Logic, your Email Sending code, and your HTTP Response code all inside a single app.js file, your project will become a "Big Ball of Mud".

When a company scales from 1 backend developer to 100 developers, the codebase must be mathematically divided into strict, isolated Layers. This is Clean Architecture (also known as Onion Architecture or Hexagonal Architecture).


1️⃣ The MVC Pattern (The Standard Starter)

Model-View-Controller (MVC) is the classic paradigm.

  1. Model (Database): The raw structure of the data (e.g., Prisma schema or Mongoose model).
  2. View (UI): In modern tech, this is the React frontend. Express doesn't handle Views anymore.
  3. Controller (Logic): The Express route function that receives the HTTP request, asks the Model for data, and returns JSON.

The Problem with MVC in Express

Most developers dump 200 lines of complex business logic directly inside the Controller. "If user buys cart > check inventory > deduct credit card > send email > write to database". This makes the Controller completely untestable and violently tied to Express.


2️⃣ The N-Tier Enterprise Architecture

To fix the MVC bloat, Professional backends introduce a new layer holding the Core Business Logic: The Service Layer.

Layer 1: The Express Router

Its only job is connecting URLs to functions and assigning Middlewares. It does zero math.

// routes/userRoutes.js
import express from 'express';
import { createUser } from '../controllers/userController.js';
import { requireAuth } from '../middleware/auth.js';

const router = express.Router();
router.post('/register', requireAuth, createUser);
export default router;

Layer 2: The Controller

Its only job is parsing HTTP Requests (req.body, req.params) and sending HTTP Responses (res.status(200)). It does NOT know about the database. In fact, you should be able to swap Express for Fastify or a GraphQL server without modifying a single drop of business logic.

// controllers/userController.js
import { UserService } from '../services/userService.js';

export const createUser = async (req, res) => {
    try {
        // Strip away 'req' and 'res'. Pass raw data to the core Service.
        const result = await UserService.register(req.body.email, req.body.password);
        
        // Handle the HTTP response
        res.status(201).json({ success: true, data: result });
    } catch (err) {
        res.status(400).json({ error: err.message });
    }
};

Layer 3: The Service (The Core Application)

This handles the heavy lifting: validating rules, checking inventory, processing payments. It knows absolutely nothing about HTTP, req, or res.

// services/userService.js
import { db } from '../data/database.js';

export class UserService {
    static async register(email, plainPassword) {
        // Business Logic 1: Does user exist?
        const exists = await db.users.find(email);
        if (exists) throw new Error("Email already taken");

        // Business Logic 2: Hash password
        const securePassword = await hash(plainPassword);

        // Data Access: Save to DB
        return await db.users.create({ email, password: securePassword });
    }
}

3️⃣ Dependency Injection (The Absolute Peak)

If your UserService directly imports the Postgres Database file, it is permanently locked to Postgres. If the CTO decides to migrate to MongoDB, every single file in the application breaks.

Dependency Injection (DI) solves this. Instead of a Service importing a database, you inject the database into the Service from the outside.

// The Service simply demands ANY database object that possesses a '.saveUser()' method.
class AdvancedUserService {
    constructor(databaseEngine) {
        this.db = databaseEngine;
    }

    async registerUser(email) {
        await this.db.saveUser(email);
    }
}

// In the App setup:
// If we switch to MongoDB, we just pass in a new MongoEngine here! The UserService code never changes!
const postgresEngine = new PostgresDB();
const myService = new AdvancedUserService(postgresEngine);

4️⃣ Environment Variables (.env)

Never hardcode passwords or API keys in your code. Hackers have bots that scrape GitHub 24/7 searching for leaked database passwords.

Use a .env file to hold secret strings. This file is always added to .gitignore.

# .env file (DO NOT COMMIT to Git)
DATABASE_URL=postgres://admin:superSecretPassword@aws.com/mydb
STRIPE_API_KEY=sk_test_123456789
PORT=8080

Node.js reads these securely using the dotenv package.

import dotenv from 'dotenv';
dotenv.config();

// Safely access the invisible password
console.log(process.env.DATABASE_URL);

💡 Summary Architectural Tenets

  • Single Responsibility: A file does exactly one thing. A router routes. A controller parses HTTP. A service calculates math.
  • Agnostic Logic: The Core Service Layer should not import express. It should just take in raw parameters and return raw data.
  • Dependency Injection: Pass databases into functions instead of hardcoding imports, making testing incredibly easy.
  • Secrets Security: Store all API keys, JWT Secrets, and Database URLs in a gitignored .env file.

Knowledge Check

Complete this quick quiz to verify your understanding and unlock the next module.