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 |
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.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
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 |