Project Structure — Monorepo Layout and Folder Conventions

▶ Try It Yourself

How you organise your project files determines how easy the codebase is to navigate, how well it scales as features are added, and how quickly a new developer can understand the project. A poorly structured project turns every feature addition into an archaeological dig through unrelated files. A well-structured project makes the purpose of every file obvious from its location alone. In this lesson you will learn two approaches — separate repositories and a monorepo — understand the conventions used inside each layer of the MEAN Stack, and establish the folder structure that the rest of this course builds on.

Repository Strategies

Strategy Structure Best For Drawback
Monorepo One repo: /frontend and /backend Small teams, learning, shared types Git history mixes frontend and backend
Polyrepo Two repos: app-frontend and app-backend Large teams, independent deployments Cross-repo changes need two PRs
Nx Monorepo One repo with Nx tooling Enterprise, shared libraries More tooling overhead

Express Backend Patterns

Pattern Structure Best For
MVC (Model-View-Controller) Routes → Controllers → Models Standard REST APIs — most common
Feature Folders Each feature has its own route, controller, model Large APIs with many resources
Layered Architecture Routes → Controllers → Services → Repositories → Models Complex business logic, testability

Angular Module Strategies (Angular 17+)

Approach Description When to Use
Standalone Components Components declare their own imports — no NgModule needed New projects with Angular 14+
Feature Modules Related components grouped in NgModule Large apps with lazy loading
Core + Shared Modules CoreModule for singletons, SharedModule for reusables Any project size
Note: The folder structure shown in this lesson is the MVC (Model-View-Controller) pattern for Express, combined with the Core/Features/Shared pattern for Angular. Both are well-established conventions used in real production codebases. Once you understand why each folder exists, you can adapt the structure to any project’s needs.
Tip: Name files consistently using the same convention throughout the project. For Express, use kebab-case for filenames: task.routes.js, task.controller.js, task.model.js. For Angular, use the Angular CLI’s naming convention: task-list.component.ts, task.service.ts, task.model.ts. Consistent naming makes files predictable to find.
Warning: Avoid putting all your logic in route handler functions (also called “fat routes”). Route handlers should be thin — read the request, call the appropriate controller function, send the response. Business logic belongs in controllers or services. Database queries belong in services or model methods. Fat routes are impossible to test and painful to maintain.

Complete Project Structure

task-manager/                       ← Root of the monorepo
│
├── .gitignore                      ← Ignore node_modules, .env, dist
├── README.md
├── package.json                    ← Root scripts (run both servers)
│
├── backend/                        ← Express + Node.js API
│   ├── src/
│   │   ├── index.js                ← App entry point — start server
│   │   ├── app.js                  ← Express app setup — middleware, routes
│   │   │
│   │   ├── config/
│   │   │   ├── database.js         ← MongoDB connection logic
│   │   │   └── env.js              ← Validated environment variables
│   │   │
│   │   ├── models/                 ← Mongoose schemas and models
│   │   │   ├── user.model.js
│   │   │   └── task.model.js
│   │   │
│   │   ├── controllers/            ← Request handlers (thin — call services)
│   │   │   ├── auth.controller.js
│   │   │   └── task.controller.js
│   │   │
│   │   ├── services/               ← Business logic (reusable, testable)
│   │   │   ├── auth.service.js
│   │   │   └── task.service.js
│   │   │
│   │   ├── routes/                 ← Express Router definitions
│   │   │   ├── auth.routes.js
│   │   │   └── task.routes.js
│   │   │
│   │   ├── middleware/             ← Reusable middleware
│   │   │   ├── authenticate.js     ← Verify JWT token
│   │   │   ├── validate.js         ← express-validator error handler
│   │   │   └── errorHandler.js     ← Global error middleware
│   │   │
│   │   └── utils/
│   │       ├── response.js         ← Consistent JSON response helpers
│   │       ├── asyncHandler.js     ← Wrap async route handlers
│   │       └── logger.js           ← Logging utility
│   │
│   ├── tests/
│   │   ├── auth.test.js
│   │   └── task.test.js
│   │
│   ├── .env                        ← Environment variables (git-ignored)
│   ├── .env.example                ← Template for teammates
│   ├── .eslintrc.js
│   └── package.json
│
└── frontend/                       ← Angular application
    ├── src/
    │   ├── main.ts                 ← Bootstrap Angular application
    │   ├── index.html
    │   ├── styles.scss             ← Global styles
    │   │
    │   └── app/
    │       ├── app.config.ts       ← App providers (router, http, etc.)
    │       ├── app.routes.ts       ← Top-level route definitions
    │       ├── app.component.ts    ← Root component
    │       │
    │       ├── core/               ← Singleton services — loaded once
    │       │   ├── services/
    │       │   │   ├── auth.service.ts
    │       │   │   └── api.service.ts
    │       │   ├── interceptors/
    │       │   │   ├── auth.interceptor.ts   ← Attach JWT to requests
    │       │   │   └── error.interceptor.ts  ← Global error handling
    │       │   └── guards/
    │       │       └── auth.guard.ts         ← Protect routes
    │       │
    │       ├── features/           ← Feature modules / standalone components
    │       │   ├── auth/
    │       │   │   ├── login/
    │       │   │   │   ├── login.component.ts
    │       │   │   │   └── login.component.html
    │       │   │   └── register/
    │       │   │       ├── register.component.ts
    │       │   │       └── register.component.html
    │       │   │
    │       │   └── tasks/
    │       │       ├── task-list/
    │       │       ├── task-form/
    │       │       └── task-detail/
    │       │
    │       └── shared/             ← Reusable UI components
    │           ├── components/
    │           │   ├── button/
    │           │   ├── input/
    │           │   └── spinner/
    │           ├── models/
    │           │   ├── task.model.ts    ← TypeScript interfaces
    │           │   └── user.model.ts
    │           └── pipes/
    │
    ├── environments/
    │   ├── environment.ts           ← Development config
    │   └── environment.prod.ts      ← Production config
    │
    ├── angular.json
    └── package.json

Key Files Explained

// backend/src/app.js — Express application setup
const express     = require('express');
const cors        = require('cors');
const helmet      = require('helmet');
const authRoutes  = require('./routes/auth.routes');
const taskRoutes  = require('./routes/task.routes');
const errorHandler= require('./middleware/errorHandler');

const app = express();

// Global middleware — order matters
app.use(helmet());                                      // security headers
app.use(cors({ origin: process.env.FRONTEND_URL }));    // allow Angular
app.use(express.json({ limit: '10mb' }));               // parse JSON body
app.use(express.urlencoded({ extended: true }));        // parse form data

// Routes
app.use('/api/auth',  authRoutes);
app.use('/api/tasks', taskRoutes);

// 404 handler — must be after all routes
app.use((req, res) => {
    res.status(404).json({ success: false, message: 'Route not found' });
});

// Global error handler — must be last, has 4 params
app.use(errorHandler);

module.exports = app;

// backend/src/index.js — entry point
const app        = require('./app');
const connectDB  = require('./config/database');

const PORT = process.env.PORT || 3000;

async function startServer() {
    await connectDB();                  // connect to MongoDB first
    app.listen(PORT, () => {
        console.log(`API running on port ${PORT}`);
    });
}

startServer();
// backend/src/utils/asyncHandler.js
// Wraps async route handlers — eliminates try/catch boilerplate
const asyncHandler = fn => (req, res, next) =>
    Promise.resolve(fn(req, res, next)).catch(next);

module.exports = asyncHandler;

// Usage in a route:
const asyncHandler = require('../utils/asyncHandler');
router.get('/', asyncHandler(async (req, res) => {
    const tasks = await Task.find();    // any error automatically calls next(err)
    res.json({ success: true, data: tasks });
}));

// backend/src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
    console.error(err.stack);

    const status  = err.status  ?? 500;
    const message = err.message ?? 'Internal Server Error';

    // Never expose stack trace to client in production
    res.status(status).json({
        success: false,
        message: process.env.NODE_ENV === 'production' ? 'Server Error' : message,
        ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
    });
};
// frontend/src/environments/environment.ts
export const environment = {
    production: false,
    apiUrl:     'http://localhost:3000/api',
};

// frontend/src/environments/environment.prod.ts
export const environment = {
    production: true,
    apiUrl:     'https://api.yourapp.com/api',
};

// frontend/src/app/shared/models/task.model.ts
export interface Task {
    _id:        string;
    title:      string;
    description?: string;
    priority:   'low' | 'medium' | 'high';
    status:     'pending' | 'in-progress' | 'completed';
    dueDate?:   string;
    user:       string;
    createdAt:  string;
    updatedAt:  string;
}

export interface CreateTaskDto {
    title:       string;
    description?: string;
    priority?:   Task['priority'];
    dueDate?:    string;
}

How It Works

Step 1 — Separation of Concerns Drives the Folder Structure

Each folder has one type of responsibility. models/ defines data shape. controllers/ handles HTTP. services/ contains business logic. routes/ declares URLs. When a bug occurs, you go directly to the relevant folder rather than searching through a monolithic file. When adding a new feature, you know exactly where each piece of code belongs.

Step 2 — The asyncHandler Eliminates Try/Catch Boilerplate

Every async route handler needs try/catch to avoid unhandled Promise rejections. Writing it repeatedly clutters the code. The asyncHandler wrapper calls the handler inside a Promise and routes any rejection to Express’s next(err) automatically. One global error handler at the bottom of app.js then formats and sends all errors consistently.

Step 3 — Environment Files Handle Dev vs Production Config

Angular’s environment.ts contains development values (localhost:3000). environment.prod.ts contains production values (your deployed API URL). The Angular CLI automatically replaces the file at build time: ng build --configuration production swaps in the production environment. Services import from environment.ts — they never need to know about production values directly.

Step 4 — TypeScript Interfaces Define the API Contract

Defining Task and CreateTaskDto interfaces in shared/models/ creates a single source of truth for the shape of your data. Every service, component, and test file imports from this location. When the API changes a field name, TypeScript immediately highlights every file that needs updating — turning runtime surprises into compile-time errors.

Step 5 — Core vs Features vs Shared Is a Proven Angular Pattern

core/ contains singleton services loaded once (AuthService, ApiService, interceptors, guards). features/ contains the UI for each distinct section of the app — each feature can be lazy-loaded. shared/ contains UI components and utilities used across features. This three-part organisation prevents circular dependencies and makes it clear whether a change affects the whole app or just one feature.

Common Mistakes

Mistake 1 — Putting all routes in one file

❌ Wrong — one file grows endlessly:

// index.js — one file with 50 routes
app.get('/api/users', ...);
app.post('/api/users', ...);
app.get('/api/tasks', ...);
app.post('/api/tasks', ...);
// 40 more routes...

✅ Correct — one router file per resource:

// routes/user.routes.js — only user routes
// routes/task.routes.js — only task routes
app.use('/api/users', userRoutes);
app.use('/api/tasks', taskRoutes);

Mistake 2 — Importing environment variables directly from process.env in every file

❌ Wrong — typos and missing values discovered at runtime:

// Scattered across multiple files — no central validation
const secret = process.env.JWT_SCRET;  // typo — undefined at runtime!

✅ Correct — validate and export all env vars from one config file:

// config/env.js
if (!process.env.JWT_SECRET) throw new Error('JWT_SECRET is required');
module.exports = {
    jwtSecret:   process.env.JWT_SECRET,
    mongoUri:    process.env.MONGODB_URI,
    port:        Number(process.env.PORT) || 3000,
};

Mistake 3 — Keeping Angular services in the feature folder when they should be in core

❌ Wrong — AuthService inside features/auth/ cannot be easily used by other features:

features/auth/auth.service.ts   ← scoped to auth feature only
features/tasks/task.service.ts  ← tasks need auth — circular dependency risk

✅ Correct — singleton services belong in core/:

core/services/auth.service.ts   ← available to all features
core/services/task.service.ts   ← available to all features

▶ Try It Yourself

Quick Reference — Folder Purpose

Folder Layer Contains
backend/src/models/ Backend Mongoose schemas and model exports
backend/src/controllers/ Backend req/res handlers — thin, delegate to services
backend/src/services/ Backend Business logic — reusable, testable
backend/src/routes/ Backend URL and method → controller mappings
backend/src/middleware/ Backend Auth, validation, error handling
frontend/src/app/core/ Frontend Singleton services, interceptors, guards
frontend/src/app/features/ Frontend Page components grouped by feature
frontend/src/app/shared/ Frontend Reusable UI components, models, pipes

🧠 Test Yourself

Where should the business logic for hashing a password before saving a user belong in an Express MVC structure?





▶ Try It Yourself