Skip to content

Mean 20 Project

DodaTech 6 min read

title: "MEAN Stack Mini Project — Building a Complete Application" description: "Build a complete MEAN Stack application from scratch combining MongoDB, Express, Angular, and Node.js into a full-stack task management system." weight: 30 date: 2026-06-28 lastmod: 2026-06-28 tags: [frameworks, mean]

This mini project guides you through building a complete task management application with the MEAN stack, combining all concepts from this course.

What You'll Learn

You will apply all MEAN concepts in a real project: Express API with CRUD, Mongoose models, Angular components and services, JWT authentication, and deployment.

Why It Matters

Building a complete project solidifies your understanding of how all MEAN layers work together. You will have a working application you can extend and deploy.

Real-World Use

DodaZIP uses a similar task management system internally for tracking feature requests, assigning work to team members, and monitoring project progress.

flowchart TD
    A[Task Manager App] --> B[Express API]
    A --> C[Angular Frontend]
    B --> D[MongoDB Atlas]
    B --> E[JWT Auth]
    C --> F[Auth Components]
    C --> G[Task CRUD]
    C --> H[User Dashboard]
    style A fill:#4a90d9,color:#fff

Project Structure

Organize the project with separate backend and frontend directories.

mean-task-manager/
  backend/
    config/
      database.js
      auth.js
      upload.js
    middleware/
      auth.js
      authorize.js
      errorHandler.js
    models/
      User.js
      Task.js
    routes/
      authRoutes.js
      taskRoutes.js
    utils/
      AppError.js
      asyncHandler.js
    server.js
    .env
  frontend/
    src/
      app/
        services/
          auth.service.ts
          task.service.ts
        components/
          login/
          register/
          dashboard/
          task-list/
          task-form/
        guards/
          auth.guard.ts
        interceptors/
          auth.interceptor.ts
      environments/

Expected output: A clean project structure separating backend Express API and frontend Angular application.

Backend Setup

Create the Express server with MongoDB connection, CORS, and error handling.

// backend/server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

const authRoutes = require('./routes/authRoutes');
const taskRoutes = require('./routes/taskRoutes');
const errorHandler = require('./middleware/errorHandler');

const app = express();
app.use(cors());
app.use(express.json());

mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'));

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

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Expected output: Express server with CORS, JSON parsing, auth and task routes, and error handling.

Task Model

Create the Task Mongoose model.

// backend/models/Task.js
const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true },
  description: { type: String, default: '' },
  status: {
    type: String,
    enum: ['todo', 'in-progress', 'done'],
    default: 'todo'
  },
  priority: {
    type: String,
    enum: ['low', 'medium', 'high'],
    default: 'medium'
  },
  assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  dueDate: Date
}, { timestamps: true });

taskSchema.index({ createdBy: 1, status: 1 });
taskSchema.index({ assignedTo: 1 });

module.exports = mongoose.model('Task', taskSchema);

Expected output: Task model with title, description, status, priority, assignments, and due date. Indexes for efficient queries.

Task API Routes

Create CRUD routes for tasks with authentication.

// backend/routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const Task = require('../models/Task');
const { authMiddleware } = require('../middleware/auth');
const asyncHandler = require('../utils/asyncHandler');

router.use(authMiddleware);

router.get('/', asyncHandler(async (req, res) => {
  const { status, priority, page = 1, limit = 10 } = req.query;
  const filter = { createdBy: req.user._id };
  if (status) filter.status = status;
  if (priority) filter.priority = priority;

  const [tasks, total] = await Promise.all([
    Task.find(filter).sort({ createdAt: -1 }).skip((page - 1) * limit).limit(Number(limit)),
    Task.countDocuments(filter)
  ]);

  res.json({ success: true, data: tasks, pagination: { page: Number(page), limit: Number(limit), total, pages: Math.ceil(total / limit) } });
}));

router.post('/', asyncHandler(async (req, res) => {
  const task = await Task.create({ ...req.body, createdBy: req.user._id });
  res.status(201).json({ success: true, data: task });
}));

router.put('/:id', asyncHandler(async (req, res) => {
  const task = await Task.findOneAndUpdate(
    { _id: req.params.id, createdBy: req.user._id },
    req.body,
    { new: true, runValidators: true }
  );
  if (!task) return res.status(404).json({ success: false, error: 'Task not found' });
  res.json({ success: true, data: task });
}));

router.delete('/:id', asyncHandler(async (req, res) => {
  const task = await Task.findOneAndDelete({ _id: req.params.id, createdBy: req.user._id });
  if (!task) return res.status(404).json({ success: false, error: 'Task not found' });
  res.status(204).send();
}));

module.exports = router;

Expected output: Full CRUD for tasks with authentication, filtering, pagination, and user scoping.

Angular Task Service

Create the Angular service for task API calls.

// frontend/src/app/services/task.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

export interface Task {
  _id: string;
  title: string;
  description: string;
  status: 'todo' | 'in-progress' | 'done';
  priority: 'low' | 'medium' | 'high';
  dueDate?: string;
  createdAt: string;
}

@Injectable({ providedIn: 'root' })
export class TaskService {
  private apiUrl = `${environment.apiUrl}/tasks`;

  constructor(private http: HttpClient) {}

  getTasks(params?: any): Observable<any> {
    let httpParams = new HttpParams();
    if (params) Object.keys(params).forEach(k => httpParams = httpParams.set(k, params[k]));
    return this.http.get(this.apiUrl, { params: httpParams });
  }

  createTask(task: Partial<Task>): Observable<any> {
    return this.http.post(this.apiUrl, task);
  }

  updateTask(id: string, task: Partial<Task>): Observable<any> {
    return this.http.put(`${this.apiUrl}/${id}`, task);
  }

  deleteTask(id: string): Observable<any> {
    return this.http.delete(`${this.apiUrl}/${id}`);
  }
}

Expected output: Typed Angular service with CRUD methods for tasks, supporting query parameters for filtering.

Angular Dashboard Component

Build the main dashboard component.

// frontend/src/app/components/dashboard/dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TaskService, Task } from '../../services/task.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [CommonModule, RouterModule],
  template: `
    <div class="dashboard">
      <h1>Task Dashboard</h1>
      <div class="stats" *ngIf="stats">
        <div class="stat-card">Total: {{ stats.total }}</div>
        <div class="stat-card todo">Todo: {{ stats.todo }}</div>
        <div class="stat-card progress">In Progress: {{ stats.inProgress }}</div>
        <div class="stat-card done">Done: {{ stats.done }}</div>
      </div>
      <div class="actions">
        <a routerLink="/tasks/new" class="btn">New Task</a>
      </div>
      <div class="task-list">
        <div *ngFor="let task of tasks" class="task-card">
          <h3>{{ task.title }}</h3>
          <span class="badge" [class]="task.priority">{{ task.priority }}</span>
          <span class="badge" [class]="task.status">{{ task.status }}</span>
          <p>{{ task.description }}</p>
          <a [routerLink]="['/tasks', task._id]">View</a>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .stats { display: flex; gap: 16px; margin: 16px 0; }
    .stat-card { padding: 16px; border-radius: 8px; background: #f0f0f0; flex: 1; }
    .todo { border-left: 4px solid #ffd700; }
    .progress { border-left: 4px solid #87ceeb; }
    .done { border-left: 4px solid #90ee90; }
    .task-card { border: 1px solid #ddd; padding: 16px; margin: 8px 0; border-radius: 8px; }
    .badge { padding: 2px 8px; border-radius: 4px; font-size: 12px; margin: 0 4px; }
    .high { background: #ff4444; color: white; }
    .medium { background: #ffaa00; }
    .low { background: #90ee90; }
    .btn { padding: 8px 16px; background: #4a90d9; color: white; text-decoration: none; border-radius: 4px; }
  `]
})
export class DashboardComponent implements OnInit {
  tasks: Task[] = [];
  stats: any = null;

  constructor(private taskService: TaskService) {}

  ngOnInit() {
    this.taskService.getTasks({ limit: 50 }).subscribe(res => {
      this.tasks = res.data;
      this.stats = {
        total: res.pagination.total,
        todo: res.data.filter((t: Task) => t.status === 'todo').length,
        inProgress: res.data.filter((t: Task) => t.status === 'in-progress').length,
        done: res.data.filter((t: Task) => t.status === 'done').length
      };
    });
  }
}

Expected output: Dashboard with stats cards showing task counts by status, recent task list, and a create button.

Common Mistakes

  1. Not scoping tasks to users: Each task should be associated with a user. Filter by createdBy to ensure users only see their own tasks.

  2. Not validating task ownership on updates: Verify the task belongs to the user before updating or deleting. Prevent users from modifying other users' tasks.

  3. Not implementing route guards on the frontend: Protect Angular routes so unauthenticated users are redirected to login.

  4. Not handling loading and error states: Every component should show loading indicators and handle API errors gracefully.

  5. Not refreshing the task list after mutations: After creating, updating, or deleting a task, refresh the list to show current data.

Challenge

Extend the task manager with: user assignment (assign tasks to other users), comments on tasks, file attachments, due date notifications, and a Kanban board view.

What's Next

Congratulations on completing the MEAN Stack course. Continue with Angular Guide for deeper Angular knowledge or Express Guide for advanced backend patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro