# MEVN Stack — Complete Beginner-to-Intermediate Project Guide

> **Project**: TaskFlow — A full-stack Task Management app with user authentication  
> **Stack**: MongoDB · Express.js · Vue 3 · Node.js  
> **Features**: JWT Auth, CRUD, Pinia, Vue Router, Tailwind CSS, Mongoose Transactions

---

## Table of Contents

1. [Project Overview & Structure](#step-1-project-overview--structure)
2. [Backend: Server Setup & Config](#step-2-backend-server-setup--config)
3. [Backend: Database Models](#step-3-database-models)
4. [Backend: Auth Controllers & Routes](#step-4-authentication-controllers--routes)
5. [Backend: Task Controllers & Routes](#step-5-task-controllers--routes)
6. [Backend: Middleware](#step-6-middleware)
7. [Frontend: Vue 3 Setup](#step-7-frontend-vue-3-setup)
8. [Frontend: Pinia Stores & Axios](#step-8-pinia-stores--axios)
9. [Frontend: Vue Router & Guards](#step-9-vue-router--navigation-guards)
10. [Frontend: Auth Pages (Login/Register)](#step-10-auth-pages)
11. [Frontend: Dashboard & Task UI](#step-11-dashboard--task-ui)
12. [Running the Project](#step-12-running-the-project)
13. [Summary, Extensions & Best Practices](#step-13-summary-extensions--best-practices)

---

## Step 1: Project Overview & Structure

### What We're Building

**TaskFlow** is a task management application where users can:
- Register and log in securely (bcrypt + JWT)
- Create, read, update, and delete tasks
- Set task priorities, due dates, and statuses
- See only their own tasks (user-scoped data)

### Final Folder Structure

```
taskflow/
├── server/                       ← Node.js / Express backend
│   ├── config/
│   │   └── db.js                 ← MongoDB connection
│   ├── controllers/
│   │   ├── authController.js
│   │   └── taskController.js
│   ├── middleware/
│   │   ├── authMiddleware.js     ← JWT verification
│   │   ├── errorMiddleware.js
│   │   └── validateMiddleware.js
│   ├── models/
│   │   ├── User.js
│   │   └── Task.js
│   ├── routes/
│   │   ├── authRoutes.js
│   │   └── taskRoutes.js
│   ├── .env
│   ├── package.json
│   └── server.js                 ← Express entry point
│
└── client/                       ← Vue 3 frontend
    ├── public/
    ├── src/
    │   ├── api/
    │   │   └── axios.js          ← Axios instance + interceptors
    │   ├── assets/
    │   ├── components/
    │   │   ├── TaskCard.vue
    │   │   ├── TaskModal.vue
    │   │   ├── Navbar.vue
    │   │   └── LoadingSpinner.vue
    │   ├── router/
    │   │   └── index.js          ← Vue Router + nav guards
    │   ├── stores/
    │   │   ├── authStore.js      ← Pinia auth store
    │   │   └── taskStore.js      ← Pinia task store
    │   ├── views/
    │   │   ├── LoginView.vue
    │   │   ├── RegisterView.vue
    │   │   └── DashboardView.vue
    │   ├── App.vue
    │   └── main.js
    ├── index.html
    ├── tailwind.config.js
    ├── vite.config.js
    └── package.json
```

---

## Step 2: Backend Server Setup & Config

### 2.1 Initialize the Backend

```bash
# Create project root
mkdir taskflow && cd taskflow

# Create and enter the server directory
mkdir server && cd server

# Initialize Node.js project
npm init -y

# Install ALL backend dependencies
npm install express mongoose bcryptjs jsonwebtoken dotenv cors helmet \
            express-validator morgan express-async-handler

# Install dev dependencies
npm install --save-dev nodemon
```

### 2.2 `server/package.json` — Add Scripts

```json
{
  "name": "taskflow-server",
  "version": "1.0.0",
  "description": "TaskFlow REST API",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-async-handler": "^1.2.0",
    "express-validator": "^7.0.1",
    "helmet": "^7.0.0",
    "jsonwebtoken": "^9.0.0",
    "mongoose": "^7.5.0",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
```

### 2.3 `server/.env` — Environment Variables

```env
# Server
NODE_ENV=development
PORT=5000

# Database
MONGO_URI=mongodb://127.0.0.1:27017/taskflow
# For MongoDB Atlas, use:
# MONGO_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/taskflow

# JWT — CHANGE THESE IN PRODUCTION (use long random strings)
JWT_SECRET=your_super_secret_jwt_key_change_me_in_production
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=your_refresh_secret_key
JWT_REFRESH_EXPIRES_IN=30d

# CORS
CLIENT_ORIGIN=http://localhost:5173
```

> **Security note for students**: Never commit `.env` to git.  
> Add `.env` to your `.gitignore` immediately.

### 2.4 `server/config/db.js` — MongoDB Connection

```javascript
// config/db.js
// This module handles connecting to MongoDB using Mongoose.
// We export a function so server.js can call it at startup.

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    // mongoose.connect returns a promise — we await it
    const conn = await mongoose.connect(process.env.MONGO_URI, {
      // These options suppress deprecation warnings
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    console.log(`✅ MongoDB connected: ${conn.connection.host}`);
  } catch (error) {
    // If DB connection fails, there's no point running the server
    console.error(`❌ MongoDB connection error: ${error.message}`);
    process.exit(1); // Exit with failure code
  }
};

module.exports = connectDB;
```

### 2.5 `server/server.js` — Express Entry Point

```javascript
// server.js
// The main entry point. Sets up Express middleware, routes, and starts listening.

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const dotenv = require('dotenv');

// Load environment variables FIRST — before importing anything that uses them
dotenv.config();

const connectDB = require('./config/db');
const authRoutes = require('./routes/authRoutes');
const taskRoutes = require('./routes/taskRoutes');
const { errorHandler, notFound } = require('./middleware/errorMiddleware');

// Connect to MongoDB
connectDB();

const app = express();

// ─── Security Middleware ──────────────────────────────────────────────────────
// helmet() sets secure HTTP headers automatically
app.use(helmet());

// cors() allows our Vue frontend (different port) to call this API
app.use(cors({
  origin: process.env.CLIENT_ORIGIN,
  credentials: true, // Allow cookies / auth headers
}));

// ─── Request Parsing Middleware ───────────────────────────────────────────────
// These two lines let us read JSON and URL-encoded bodies from requests
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// ─── Logging Middleware ───────────────────────────────────────────────────────
// morgan('dev') logs: METHOD /path STATUS ms
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

// ─── Health Check Route ───────────────────────────────────────────────────────
// Useful for checking if the server is running without hitting a real endpoint
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', message: 'TaskFlow API running' });
});

// ─── API Routes ───────────────────────────────────────────────────────────────
app.use('/api/auth', authRoutes);   // POST /api/auth/register, /api/auth/login
app.use('/api/tasks', taskRoutes);  // GET/POST/PUT/DELETE /api/tasks

// ─── Error Handling (must be LAST middleware) ─────────────────────────────────
app.use(notFound);     // Handle 404 — route not found
app.use(errorHandler); // Handle all thrown errors

// ─── Start Server ─────────────────────────────────────────────────────────────
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
  console.log(`🚀 Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
```

---

## Step 3: Database Models

### 3.1 `server/models/User.js`

```javascript
// models/User.js
// Defines the shape of user documents in MongoDB using Mongoose Schema.
// Also handles password hashing automatically before saving.

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, 'Name is required'],
      trim: true,              // Remove leading/trailing whitespace
      minlength: [2, 'Name must be at least 2 characters'],
      maxlength: [50, 'Name cannot exceed 50 characters'],
    },
    email: {
      type: String,
      required: [true, 'Email is required'],
      unique: true,            // Enforce unique emails at DB level
      lowercase: true,         // Always store as lowercase
      trim: true,
      match: [
        /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
        'Please provide a valid email',
      ],
    },
    password: {
      type: String,
      required: [true, 'Password is required'],
      minlength: [6, 'Password must be at least 6 characters'],
      select: false,           // IMPORTANT: password is never returned in queries by default
    },
    avatar: {
      type: String,
      default: '', // Could store a URL to a profile picture
    },
    refreshToken: {
      type: String,
      select: false,           // Never expose the refresh token
    },
  },
  {
    // Automatically adds createdAt and updatedAt timestamps
    timestamps: true,
  }
);

// ─── Pre-save Hook ────────────────────────────────────────────────────────────
// This runs BEFORE every .save() call.
// We only re-hash the password if it was actually changed.
userSchema.pre('save', async function (next) {
  // "this" refers to the document being saved
  if (!this.isModified('password')) {
    return next(); // Password unchanged — skip hashing
  }

  // bcrypt salt rounds: 12 is secure; higher = slower (by design)
  const salt = await bcrypt.genSalt(12);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// ─── Instance Method ──────────────────────────────────────────────────────────
// Add a method directly to user documents for password comparison
userSchema.methods.comparePassword = async function (candidatePassword) {
  // bcrypt.compare handles the salt automatically — it's built into the hash
  return await bcrypt.compare(candidatePassword, this.password);
};

// ─── Virtual Field ────────────────────────────────────────────────────────────
// A computed property that doesn't get stored in the DB
userSchema.virtual('initials').get(function () {
  return this.name
    .split(' ')
    .map((n) => n[0])
    .join('')
    .toUpperCase();
});

const User = mongoose.model('User', userSchema);
module.exports = User;
```

### 3.2 `server/models/Task.js`

```javascript
// models/Task.js
// Defines tasks. Each task belongs to a user (via ObjectId reference).
// This is the "one-to-many" relationship: one user has many tasks.

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema(
  {
    title: {
      type: String,
      required: [true, 'Task title is required'],
      trim: true,
      maxlength: [100, 'Title cannot exceed 100 characters'],
    },
    description: {
      type: String,
      trim: true,
      maxlength: [500, 'Description cannot exceed 500 characters'],
      default: '',
    },
    status: {
      type: String,
      // Only allow these specific values
      enum: {
        values: ['todo', 'in-progress', 'done'],
        message: 'Status must be todo, in-progress, or done',
      },
      default: 'todo',
    },
    priority: {
      type: String,
      enum: {
        values: ['low', 'medium', 'high'],
        message: 'Priority must be low, medium, or high',
      },
      default: 'medium',
    },
    dueDate: {
      type: Date,
      default: null,
    },
    tags: {
      type: [String], // Array of strings
      default: [],
    },
    // Reference to the User who created this task
    // This is a foreign key relationship in MongoDB
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',          // Refers to the 'User' model
      required: true,
      index: true,          // Index for fast lookups by user
    },
  },
  {
    timestamps: true,
    // toJSON: include virtuals when converting to JSON
    toJSON: { virtuals: true },
    toObject: { virtuals: true },
  }
);

// ─── Virtual Field ────────────────────────────────────────────────────────────
// isOverdue is computed at runtime, not stored in DB
taskSchema.virtual('isOverdue').get(function () {
  if (!this.dueDate || this.status === 'done') return false;
  return new Date() > this.dueDate;
});

// ─── Index for performance ────────────────────────────────────────────────────
// Compound index: queries that filter by user AND sort by createdAt are very fast
taskSchema.index({ user: 1, createdAt: -1 });
taskSchema.index({ user: 1, status: 1 });

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

---

## Step 4: Authentication Controllers & Routes

### 4.1 `server/controllers/authController.js`

```javascript
// controllers/authController.js
// Handles user registration, login, token refresh, and logout.
// express-async-handler wraps async functions so errors go to errorHandler middleware.

const asyncHandler = require('express-async-handler');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const User = require('../models/User');

// ─── Helper: Generate Tokens ──────────────────────────────────────────────────
// Access tokens are short-lived; refresh tokens long-lived
const generateAccessToken = (userId) => {
  return jwt.sign(
    { id: userId },           // Payload — what we encode in the token
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN }
  );
};

const generateRefreshToken = (userId) => {
  return jwt.sign(
    { id: userId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
  );
};

// ─── Helper: Send Token Response ─────────────────────────────────────────────
const sendTokenResponse = (user, statusCode, res) => {
  const accessToken = generateAccessToken(user._id);
  const refreshToken = generateRefreshToken(user._id);

  res.status(statusCode).json({
    success: true,
    accessToken,
    refreshToken,
    user: {
      _id: user._id,
      name: user.name,
      email: user.email,
      avatar: user.avatar,
      createdAt: user.createdAt,
    },
  });
};

// ─── @POST /api/auth/register ─────────────────────────────────────────────────
// Creates a new user + profile using a Mongoose transaction for atomicity.
// A TRANSACTION means: all operations succeed together, or none of them do.
const register = asyncHandler(async (req, res) => {
  const { name, email, password } = req.body;

  // ── Mongoose Session (Transaction) ──────────────────────────────────────────
  // Use a session when you have multiple DB writes that must succeed or fail together.
  // Here it's one write, but this pattern is essential for multi-step operations
  // like: create user + create profile + send welcome notification.
  const session = await mongoose.startSession();

  try {
    // Everything inside startTransaction() is atomic
    session.startTransaction();

    // Check if email already in use
    const existingUser = await User.findOne({ email }).session(session);
    if (existingUser) {
      // Abort the transaction — nothing gets saved
      await session.abortTransaction();
      res.status(400);
      throw new Error('Email already registered');
    }

    // Create the user (password hashed by pre-save hook in User model)
    const [user] = await User.create([{ name, email, password }], { session });

    // ─── Add more writes inside the transaction here ─────────────────────────
    // For example:
    // await Profile.create([{ user: user._id, bio: '' }], { session });
    // await Notification.create([{ user: user._id, type: 'welcome' }], { session });
    // All of these would be atomic with the user creation above.

    // Commit: write everything to the database permanently
    await session.commitTransaction();

    sendTokenResponse(user, 201, res);
  } catch (error) {
    // Something went wrong — roll back all writes in this transaction
    await session.abortTransaction();
    throw error; // Re-throw so errorHandler catches it
  } finally {
    // Always end the session
    session.endSession();
  }
});

// ─── @POST /api/auth/login ────────────────────────────────────────────────────
const login = asyncHandler(async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    res.status(400);
    throw new Error('Please provide email and password');
  }

  // "+password" explicitly selects the password field (it's select: false in schema)
  const user = await User.findOne({ email }).select('+password');

  if (!user || !(await user.comparePassword(password))) {
    // Important: give same error message for both — don't reveal if email exists
    res.status(401);
    throw new Error('Invalid credentials');
  }

  sendTokenResponse(user, 200, res);
});

// ─── @POST /api/auth/refresh ──────────────────────────────────────────────────
// Issues a new access token using the refresh token
const refresh = asyncHandler(async (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    res.status(401);
    throw new Error('No refresh token provided');
  }

  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const user = await User.findById(decoded.id);

    if (!user) {
      res.status(401);
      throw new Error('User not found');
    }

    const newAccessToken = generateAccessToken(user._id);
    res.json({ success: true, accessToken: newAccessToken });
  } catch (err) {
    res.status(401);
    throw new Error('Invalid or expired refresh token');
  }
});

// ─── @GET /api/auth/me ────────────────────────────────────────────────────────
// Returns the currently logged-in user's profile
const getMe = asyncHandler(async (req, res) => {
  // req.user is set by the auth middleware (see authMiddleware.js)
  const user = await User.findById(req.user.id);

  if (!user) {
    res.status(404);
    throw new Error('User not found');
  }

  res.json({
    success: true,
    user: {
      _id: user._id,
      name: user.name,
      email: user.email,
      avatar: user.avatar,
      createdAt: user.createdAt,
    },
  });
});

// ─── @POST /api/auth/logout ───────────────────────────────────────────────────
// With JWTs, "logout" on the server side means clearing the refresh token.
// The frontend is responsible for deleting the access token from memory.
const logout = asyncHandler(async (req, res) => {
  // In a full implementation, invalidate the refresh token in the DB here
  await User.findByIdAndUpdate(req.user.id, { refreshToken: null });
  res.json({ success: true, message: 'Logged out successfully' });
});

module.exports = { register, login, refresh, getMe, logout };
```

### 4.2 `server/routes/authRoutes.js`

```javascript
// routes/authRoutes.js
// Maps HTTP methods + paths to controller functions.
// Also runs validation middleware before the controller.

const express = require('express');
const router = express.Router();
const { register, login, refresh, getMe, logout } = require('../controllers/authController');
const { protect } = require('../middleware/authMiddleware');
const { validateRegister, validateLogin } = require('../middleware/validateMiddleware');

// Public routes (no auth required)
router.post('/register', validateRegister, register);
router.post('/login', validateLogin, login);
router.post('/refresh', refresh);

// Protected routes (JWT required)
router.get('/me', protect, getMe);
router.post('/logout', protect, logout);

module.exports = router;
```

---

## Step 5: Task Controllers & Routes

### 5.1 `server/controllers/taskController.js`

```javascript
// controllers/taskController.js
// Full CRUD for tasks. All routes are protected — req.user is always set.

const asyncHandler = require('express-async-handler');
const mongoose = require('mongoose');
const Task = require('../models/Task');

// ─── @GET /api/tasks ──────────────────────────────────────────────────────────
// Get all tasks for the logged-in user with filtering, sorting, and pagination
const getTasks = asyncHandler(async (req, res) => {
  const {
    status,
    priority,
    search,
    sortBy = 'createdAt',
    order = 'desc',
    page = 1,
    limit = 20,
  } = req.query;

  // Always filter by the authenticated user's ID
  const filter = { user: req.user.id };

  // Optionally add more filters
  if (status) filter.status = status;
  if (priority) filter.priority = priority;

  // Text search on title and description
  if (search) {
    filter.$or = [
      { title: { $regex: search, $options: 'i' } },      // case-insensitive
      { description: { $regex: search, $options: 'i' } },
    ];
  }

  // Pagination calculation
  const skip = (Number(page) - 1) * Number(limit);
  const sortOrder = order === 'asc' ? 1 : -1;

  // Run both queries concurrently with Promise.all for efficiency
  const [tasks, total] = await Promise.all([
    Task.find(filter)
      .sort({ [sortBy]: sortOrder })
      .skip(skip)
      .limit(Number(limit)),
    Task.countDocuments(filter),
  ]);

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

// ─── @GET /api/tasks/:id ──────────────────────────────────────────────────────
const getTask = asyncHandler(async (req, res) => {
  const task = await Task.findOne({
    _id: req.params.id,
    user: req.user.id, // CRITICAL: always scope to user — prevents accessing others' tasks
  });

  if (!task) {
    res.status(404);
    throw new Error('Task not found');
  }

  res.json({ success: true, task });
});

// ─── @POST /api/tasks ─────────────────────────────────────────────────────────
const createTask = asyncHandler(async (req, res) => {
  const { title, description, status, priority, dueDate, tags } = req.body;

  const task = await Task.create({
    title,
    description,
    status,
    priority,
    dueDate,
    tags,
    user: req.user.id, // Automatically assign to the logged-in user
  });

  res.status(201).json({ success: true, task });
});

// ─── @PUT /api/tasks/:id ──────────────────────────────────────────────────────
const updateTask = asyncHandler(async (req, res) => {
  const { title, description, status, priority, dueDate, tags } = req.body;

  // findOneAndUpdate with { new: true } returns the UPDATED document
  const task = await Task.findOneAndUpdate(
    { _id: req.params.id, user: req.user.id }, // Scope to user
    { title, description, status, priority, dueDate, tags },
    {
      new: true,           // Return updated doc
      runValidators: true, // Run schema validators on update
    }
  );

  if (!task) {
    res.status(404);
    throw new Error('Task not found or not authorized');
  }

  res.json({ success: true, task });
});

// ─── @DELETE /api/tasks/:id ───────────────────────────────────────────────────
const deleteTask = asyncHandler(async (req, res) => {
  const task = await Task.findOneAndDelete({
    _id: req.params.id,
    user: req.user.id,
  });

  if (!task) {
    res.status(404);
    throw new Error('Task not found or not authorized');
  }

  res.json({ success: true, message: 'Task deleted successfully' });
});

// ─── @PATCH /api/tasks/batch ──────────────────────────────────────────────────
// Batch update multiple tasks in a single transaction
// Example use case: drag multiple tasks to a new status column
const batchUpdateTasks = asyncHandler(async (req, res) => {
  const { taskIds, update } = req.body;

  if (!Array.isArray(taskIds) || taskIds.length === 0) {
    res.status(400);
    throw new Error('taskIds must be a non-empty array');
  }

  // Use a transaction — either ALL tasks update or NONE do
  const session = await mongoose.startSession();

  try {
    session.startTransaction();

    const result = await Task.updateMany(
      {
        _id: { $in: taskIds }, // Match any of the provided IDs
        user: req.user.id,     // But only if they belong to this user
      },
      { $set: update },        // Apply the update (e.g. { status: 'done' })
      { session, runValidators: true }
    );

    await session.commitTransaction();

    res.json({
      success: true,
      message: `Updated ${result.modifiedCount} tasks`,
      modifiedCount: result.modifiedCount,
    });
  } catch (error) {
    await session.abortTransaction();
    throw error;
  } finally {
    session.endSession();
  }
});

// ─── @GET /api/tasks/stats ────────────────────────────────────────────────────
// MongoDB aggregation pipeline — powerful for analytics
const getTaskStats = asyncHandler(async (req, res) => {
  const stats = await Task.aggregate([
    // Stage 1: Filter to this user's tasks only
    { $match: { user: new mongoose.Types.ObjectId(req.user.id) } },
    // Stage 2: Group by status and count
    {
      $group: {
        _id: '$status',
        count: { $sum: 1 },
      },
    },
  ]);

  // Transform array into a more useful object
  const result = { todo: 0, 'in-progress': 0, done: 0 };
  stats.forEach((s) => (result[s._id] = s.count));

  res.json({ success: true, stats: result, total: Object.values(result).reduce((a, b) => a + b, 0) });
});

module.exports = { getTasks, getTask, createTask, updateTask, deleteTask, batchUpdateTasks, getTaskStats };
```

### 5.2 `server/routes/taskRoutes.js`

```javascript
// routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const {
  getTasks, getTask, createTask, updateTask, deleteTask,
  batchUpdateTasks, getTaskStats,
} = require('../controllers/taskController');
const { protect } = require('../middleware/authMiddleware');
const { validateTask } = require('../middleware/validateMiddleware');

// ALL task routes require authentication
router.use(protect);

// Stats and batch (specific routes before :id to avoid conflicts)
router.get('/stats', getTaskStats);
router.patch('/batch', batchUpdateTasks);

// Standard CRUD
router.route('/')
  .get(getTasks)
  .post(validateTask, createTask);

router.route('/:id')
  .get(getTask)
  .put(validateTask, updateTask)
  .delete(deleteTask);

module.exports = router;
```

---

## Step 6: Middleware

### 6.1 `server/middleware/authMiddleware.js`

```javascript
// middleware/authMiddleware.js
// Verifies the JWT on every protected request.
// If valid, it attaches the user to req.user.

const asyncHandler = require('express-async-handler');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = asyncHandler(async (req, res, next) => {
  let token;

  // JWTs are sent in the Authorization header as: "Bearer <token>"
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1]; // Extract the token part
  }

  if (!token) {
    res.status(401);
    throw new Error('Not authorized — no token provided');
  }

  try {
    // jwt.verify will throw an error if the token is invalid or expired
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Attach the user to the request object (without the password field)
    // Every subsequent middleware and controller can access req.user
    req.user = await User.findById(decoded.id).select('-password');

    if (!req.user) {
      res.status(401);
      throw new Error('User no longer exists');
    }

    next(); // Move to the next middleware or route handler
  } catch (err) {
    res.status(401);
    throw new Error('Not authorized — invalid token');
  }
});

module.exports = { protect };
```

### 6.2 `server/middleware/validateMiddleware.js`

```javascript
// middleware/validateMiddleware.js
// Uses express-validator to check incoming request data.
// If validation fails, we return a 400 error with clear messages.

const { body, validationResult } = require('express-validator');

// Helper that checks for validation errors after validators run
const handleValidation = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      errors: errors.array().map((e) => ({ field: e.path, message: e.msg })),
    });
  }
  next();
};

// Validation rules for registration
const validateRegister = [
  body('name')
    .trim()
    .notEmpty().withMessage('Name is required')
    .isLength({ min: 2, max: 50 }).withMessage('Name must be 2-50 characters'),
  body('email')
    .trim()
    .notEmpty().withMessage('Email is required')
    .isEmail().withMessage('Please provide a valid email')
    .normalizeEmail(),
  body('password')
    .notEmpty().withMessage('Password is required')
    .isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
    .matches(/\d/).withMessage('Password must contain at least one number'),
  handleValidation,
];

// Validation rules for login
const validateLogin = [
  body('email').trim().notEmpty().withMessage('Email is required').isEmail(),
  body('password').notEmpty().withMessage('Password is required'),
  handleValidation,
];

// Validation rules for creating/updating tasks
const validateTask = [
  body('title')
    .trim()
    .notEmpty().withMessage('Task title is required')
    .isLength({ max: 100 }).withMessage('Title cannot exceed 100 characters'),
  body('description')
    .optional()
    .trim()
    .isLength({ max: 500 }).withMessage('Description cannot exceed 500 characters'),
  body('status')
    .optional()
    .isIn(['todo', 'in-progress', 'done']).withMessage('Invalid status'),
  body('priority')
    .optional()
    .isIn(['low', 'medium', 'high']).withMessage('Invalid priority'),
  body('dueDate')
    .optional()
    .isISO8601().withMessage('Due date must be a valid date'),
  handleValidation,
];

module.exports = { validateRegister, validateLogin, validateTask };
```

### 6.3 `server/middleware/errorMiddleware.js`

```javascript
// middleware/errorMiddleware.js
// Centralized error handling — all errors flow here.
// Express recognizes error middleware by its 4 parameters: (err, req, res, next)

const notFound = (req, res, next) => {
  // Create an error and pass it to the next error handler
  const error = new Error(`Route not found: ${req.originalUrl}`);
  res.status(404);
  next(error);
};

const errorHandler = (err, req, res, next) => {
  // If status is 200 (default), change to 500 (server error)
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;

  // Mongoose validation error — extract useful messages
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      success: false,
      message: 'Validation Error',
      errors: Object.values(err.errors).map((e) => e.message),
    });
  }

  // Mongoose duplicate key error (e.g., duplicate email)
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(400).json({
      success: false,
      message: `${field} already exists`,
    });
  }

  // Invalid MongoDB ObjectId
  if (err.name === 'CastError') {
    return res.status(400).json({
      success: false,
      message: 'Invalid ID format',
    });
  }

  res.status(statusCode).json({
    success: false,
    message: err.message || 'Server Error',
    // Only show stack trace in development
    stack: process.env.NODE_ENV === 'production' ? undefined : err.stack,
  });
};

module.exports = { notFound, errorHandler };
```

---

## Step 7: Frontend Vue 3 Setup

### 7.1 Initialize the Frontend

```bash
# From the taskflow/ root directory
npm create vite@latest client -- --template vue

cd client
npm install

# Install all frontend dependencies
npm install axios pinia vue-router @fortawesome/fontawesome-svg-core \
            @fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons \
            @fortawesome/vue-fontawesome

# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

### 7.2 `client/tailwind.config.js`

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  // Tell Tailwind where your template files are (so it can purge unused styles)
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      // Add Google Fonts as a font family
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
        display: ['Poppins', 'sans-serif'],
      },
      colors: {
        // Custom brand colors
        brand: {
          50: '#eff6ff',
          100: '#dbeafe',
          400: '#60a5fa',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          900: '#1e3a8a',
        },
      },
    },
  },
  plugins: [],
};
```

### 7.3 `client/index.html` — Google Fonts + Font Awesome

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TaskFlow — Manage Your Work</title>

    <!-- Google Fonts: Inter (body) + Poppins (headings) -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Poppins:wght@600;700&display=swap"
      rel="stylesheet"
    />

    <!-- Font Awesome CDN (alternative to npm package for simplicity) -->
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
    />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
```

### 7.4 `client/src/assets/main.css` — Tailwind Directives

```css
/* Import Tailwind's base styles, component classes, and utility classes */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom global styles */
@layer base {
  body {
    @apply font-sans bg-gray-50 text-gray-900 antialiased;
  }

  h1, h2, h3 {
    @apply font-display;
  }
}

/* Reusable component styles using @apply */
@layer components {
  .btn-primary {
    @apply inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white
           rounded-lg font-medium text-sm hover:bg-brand-700 focus:outline-none
           focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
           disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-150;
  }

  .btn-secondary {
    @apply inline-flex items-center gap-2 px-4 py-2 bg-white text-gray-700
           border border-gray-300 rounded-lg font-medium text-sm
           hover:bg-gray-50 focus:outline-none focus:ring-2
           focus:ring-brand-500 focus:ring-offset-2 transition-colors duration-150;
  }

  .btn-danger {
    @apply inline-flex items-center gap-2 px-4 py-2 bg-red-600 text-white
           rounded-lg font-medium text-sm hover:bg-red-700 transition-colors duration-150;
  }

  .input-field {
    @apply w-full px-3 py-2 border border-gray-300 rounded-lg text-sm
           focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent
           placeholder-gray-400 transition-shadow duration-150;
  }

  .card {
    @apply bg-white rounded-xl border border-gray-200 shadow-sm;
  }

  .badge-todo      { @apply bg-gray-100 text-gray-700 text-xs font-medium px-2 py-0.5 rounded-full; }
  .badge-progress  { @apply bg-blue-100 text-blue-700 text-xs font-medium px-2 py-0.5 rounded-full; }
  .badge-done      { @apply bg-green-100 text-green-700 text-xs font-medium px-2 py-0.5 rounded-full; }
  .badge-low       { @apply bg-gray-100 text-gray-600 text-xs font-medium px-2 py-0.5 rounded-full; }
  .badge-medium    { @apply bg-yellow-100 text-yellow-700 text-xs font-medium px-2 py-0.5 rounded-full; }
  .badge-high      { @apply bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full; }
}
```

### 7.5 `client/src/main.js` — App Bootstrap

```javascript
// main.js
// The entry point for the Vue 3 application.
// Here we plug in all our plugins: Pinia, Vue Router, Font Awesome.

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './assets/main.css';

// Font Awesome setup
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  faPlus, faTrash, faEdit, faCheck, faTimes, faSpinner,
  faSignOutAlt, faTasks, faUser, faChartBar, faSearch,
  faExclamationTriangle, faCalendar, faTag, faFilter,
  faSortAmountDown, faCheckCircle, faHourglass, faCircle,
  faEye, faEyeSlash, faShieldAlt,
} from '@fortawesome/free-solid-svg-icons';

// Add icons to the library (only icons added here can be used in templates)
library.add(
  faPlus, faTrash, faEdit, faCheck, faTimes, faSpinner,
  faSignOutAlt, faTasks, faUser, faChartBar, faSearch,
  faExclamationTriangle, faCalendar, faTag, faFilter,
  faSortAmountDown, faCheckCircle, faHourglass, faCircle,
  faEye, faEyeSlash, faShieldAlt
);

const app = createApp(App);

// Register plugins
app.use(createPinia()); // State management — must be before router if router uses stores
app.use(router);

// Register Font Awesome globally
app.component('font-awesome-icon', FontAwesomeIcon);

app.mount('#app');
```

---

## Step 8: Pinia Stores & Axios

### 8.1 `client/src/api/axios.js` — Axios Instance with Interceptors

```javascript
// api/axios.js
// A configured Axios instance shared across the whole app.
// Interceptors automatically attach JWTs and handle 401 errors.

import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api',
  headers: { 'Content-Type': 'application/json' },
  timeout: 10000, // 10 second timeout
});

// ─── Request Interceptor ──────────────────────────────────────────────────────
// Runs BEFORE every request is sent.
// We attach the JWT access token from localStorage automatically.
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// ─── Response Interceptor ─────────────────────────────────────────────────────
// Runs AFTER every response comes back.
// If we get a 401, we try to refresh the access token once before giving up.
let isRefreshing = false;
let failedQueue = []; // Queue of requests that failed while we were refreshing

const processQueue = (error, token = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) reject(error);
    else resolve(token);
  });
  failedQueue = [];
};

api.interceptors.response.use(
  (response) => response, // If OK, just pass it through
  async (error) => {
    const originalRequest = error.config;

    // If 401 and we haven't already retried this request
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Another refresh is already in progress — queue this request
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then((token) => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return api(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      const refreshToken = localStorage.getItem('refreshToken');

      if (!refreshToken) {
        // No refresh token — force logout
        localStorage.clear();
        window.location.href = '/login';
        return Promise.reject(error);
      }

      try {
        const { data } = await api.post('/auth/refresh', { refreshToken });
        const newToken = data.accessToken;

        localStorage.setItem('accessToken', newToken);
        api.defaults.headers.Authorization = `Bearer ${newToken}`;

        processQueue(null, newToken);
        originalRequest.headers.Authorization = `Bearer ${newToken}`;

        return api(originalRequest); // Retry the original request
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.clear();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

export default api;
```

### 8.2 `client/src/stores/authStore.js`

```javascript
// stores/authStore.js
// Pinia store for authentication state.
// Pinia replaces Vuex in Vue 3 — simpler API, better TypeScript support.

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import api from '../api/axios';

// defineStore takes an ID ('auth') and a setup function
export const useAuthStore = defineStore('auth', () => {
  // ─── State ────────────────────────────────────────────────────────────────
  // ref() makes these reactive — Vue will re-render when they change
  const user = ref(null);
  const accessToken = ref(localStorage.getItem('accessToken'));
  const refreshToken = ref(localStorage.getItem('refreshToken'));
  const isLoading = ref(false);
  const error = ref(null);

  // ─── Getters (computed) ───────────────────────────────────────────────────
  const isAuthenticated = computed(() => !!accessToken.value && !!user.value);
  const userName = computed(() => user.value?.name || '');
  const userInitials = computed(() =>
    userName.value.split(' ').map((n) => n[0]).join('').toUpperCase()
  );

  // ─── Actions ──────────────────────────────────────────────────────────────
  const setTokens = (tokens) => {
    accessToken.value = tokens.accessToken;
    refreshToken.value = tokens.refreshToken;
    localStorage.setItem('accessToken', tokens.accessToken);
    localStorage.setItem('refreshToken', tokens.refreshToken);
  };

  const register = async (name, email, password) => {
    isLoading.value = true;
    error.value = null;
    try {
      const { data } = await api.post('/auth/register', { name, email, password });
      user.value = data.user;
      setTokens(data);
      return { success: true };
    } catch (err) {
      error.value = err.response?.data?.message || 'Registration failed';
      return { success: false, error: error.value };
    } finally {
      isLoading.value = false;
    }
  };

  const login = async (email, password) => {
    isLoading.value = true;
    error.value = null;
    try {
      const { data } = await api.post('/auth/login', { email, password });
      user.value = data.user;
      setTokens(data);
      return { success: true };
    } catch (err) {
      error.value = err.response?.data?.message || 'Login failed';
      return { success: false, error: error.value };
    } finally {
      isLoading.value = false;
    }
  };

  const fetchMe = async () => {
    if (!accessToken.value) return;
    try {
      const { data } = await api.get('/auth/me');
      user.value = data.user;
    } catch {
      logout();
    }
  };

  const logout = () => {
    user.value = null;
    accessToken.value = null;
    refreshToken.value = null;
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  };

  return {
    user, accessToken, isLoading, error,
    isAuthenticated, userName, userInitials,
    register, login, fetchMe, logout,
  };
});
```

### 8.3 `client/src/stores/taskStore.js`

```javascript
// stores/taskStore.js
// Pinia store for task state — all task API calls and local state here.

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import api from '../api/axios';

export const useTaskStore = defineStore('tasks', () => {
  // ─── State ────────────────────────────────────────────────────────────────
  const tasks = ref([]);
  const currentTask = ref(null);
  const stats = ref({ todo: 0, 'in-progress': 0, done: 0, total: 0 });
  const isLoading = ref(false);
  const error = ref(null);
  const filters = ref({ status: '', priority: '', search: '' });
  const pagination = ref({ page: 1, pages: 1, total: 0 });

  // ─── Getters ──────────────────────────────────────────────────────────────
  const tasksByStatus = computed(() => ({
    todo: tasks.value.filter((t) => t.status === 'todo'),
    'in-progress': tasks.value.filter((t) => t.status === 'in-progress'),
    done: tasks.value.filter((t) => t.status === 'done'),
  }));

  const overdueTasks = computed(() =>
    tasks.value.filter((t) => t.dueDate && new Date() > new Date(t.dueDate) && t.status !== 'done')
  );

  // ─── Actions ──────────────────────────────────────────────────────────────
  const fetchTasks = async (params = {}) => {
    isLoading.value = true;
    error.value = null;
    try {
      const query = { ...filters.value, ...params };
      const { data } = await api.get('/tasks', { params: query });
      tasks.value = data.tasks;
      pagination.value = { page: data.page, pages: data.pages, total: data.total };
    } catch (err) {
      error.value = err.response?.data?.message || 'Failed to fetch tasks';
    } finally {
      isLoading.value = false;
    }
  };

  const fetchStats = async () => {
    try {
      const { data } = await api.get('/tasks/stats');
      stats.value = data.stats;
      stats.value.total = data.total;
    } catch (err) {
      console.error('Failed to fetch stats', err);
    }
  };

  const createTask = async (taskData) => {
    try {
      const { data } = await api.post('/tasks', taskData);
      tasks.value.unshift(data.task); // Add to beginning of list
      stats.value[data.task.status]++;
      stats.value.total++;
      return { success: true, task: data.task };
    } catch (err) {
      return { success: false, error: err.response?.data?.message || 'Create failed' };
    }
  };

  const updateTask = async (id, taskData) => {
    try {
      const { data } = await api.put(`/tasks/${id}`, taskData);
      const index = tasks.value.findIndex((t) => t._id === id);
      if (index !== -1) tasks.value[index] = data.task;
      return { success: true, task: data.task };
    } catch (err) {
      return { success: false, error: err.response?.data?.message || 'Update failed' };
    }
  };

  const deleteTask = async (id) => {
    try {
      const task = tasks.value.find((t) => t._id === id);
      await api.delete(`/tasks/${id}`);
      tasks.value = tasks.value.filter((t) => t._id !== id);
      if (task) { stats.value[task.status]--; stats.value.total--; }
      return { success: true };
    } catch (err) {
      return { success: false, error: err.response?.data?.message || 'Delete failed' };
    }
  };

  const setFilter = (key, value) => {
    filters.value[key] = value;
    fetchTasks();
  };

  const clearFilters = () => {
    filters.value = { status: '', priority: '', search: '' };
    fetchTasks();
  };

  return {
    tasks, currentTask, stats, isLoading, error, filters, pagination,
    tasksByStatus, overdueTasks,
    fetchTasks, fetchStats, createTask, updateTask, deleteTask,
    setFilter, clearFilters,
  };
});
```

---

## Step 9: Vue Router & Navigation Guards

### `client/src/router/index.js`

```javascript
// router/index.js
// Defines all routes and protects private pages with navigation guards.

import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '../stores/authStore';

// Lazy-loaded routes: the component is only downloaded when the route is visited.
// This improves initial page load performance (code splitting).
const LoginView = () => import('../views/LoginView.vue');
const RegisterView = () => import('../views/RegisterView.vue');
const DashboardView = () => import('../views/DashboardView.vue');

const routes = [
  {
    path: '/',
    redirect: '/dashboard', // Redirect root to dashboard
  },
  {
    path: '/login',
    name: 'Login',
    component: LoginView,
    meta: { requiresGuest: true }, // Only for unauthenticated users
  },
  {
    path: '/register',
    name: 'Register',
    component: RegisterView,
    meta: { requiresGuest: true },
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: DashboardView,
    meta: { requiresAuth: true }, // Requires authentication
  },
  {
    // Catch-all route for 404s
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    redirect: '/dashboard',
  },
];

const router = createRouter({
  history: createWebHistory(), // HTML5 history mode (no # in URLs)
  routes,
  // Smooth scroll to top on every navigation
  scrollBehavior: () => ({ top: 0, behavior: 'smooth' }),
});

// ─── Navigation Guard ─────────────────────────────────────────────────────────
// beforeEach runs before EVERY route navigation.
// to = where we're going, from = where we came from, next = proceed
router.beforeEach(async (to) => {
  const authStore = useAuthStore();

  // If we have a token but no user data yet, fetch user profile
  if (authStore.accessToken && !authStore.user) {
    await authStore.fetchMe();
  }

  const isAuthenticated = authStore.isAuthenticated;

  // Route requires login and user is not authenticated
  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'Login', query: { redirect: to.fullPath } };
  }

  // Route is for guests only (login/register) and user IS authenticated
  if (to.meta.requiresGuest && isAuthenticated) {
    return { name: 'Dashboard' };
  }

  // Otherwise, allow navigation
  return true;
});

export default router;
```

---

## Step 10: Auth Pages

### `client/src/views/LoginView.vue`

```vue
<!-- LoginView.vue -->
<!-- Vue 3 Composition API with <script setup> — the modern, concise way to write Vue -->
<script setup>
import { ref, reactive } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuthStore } from '../stores/authStore';

const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();

// Reactive form state using reactive() for objects
const form = reactive({ email: '', password: '' });
const showPassword = ref(false);
const errors = reactive({ email: '', password: '', general: '' });

const validateForm = () => {
  let valid = true;
  errors.email = '';
  errors.password = '';
  errors.general = '';

  if (!form.email) { errors.email = 'Email is required'; valid = false; }
  else if (!/\S+@\S+\.\S+/.test(form.email)) { errors.email = 'Invalid email format'; valid = false; }
  if (!form.password) { errors.password = 'Password is required'; valid = false; }

  return valid;
};

const handleLogin = async () => {
  if (!validateForm()) return;

  const result = await authStore.login(form.email, form.password);
  if (result.success) {
    // Redirect to the page they were trying to visit, or dashboard
    const redirect = route.query.redirect || '/dashboard';
    router.push(redirect);
  } else {
    errors.general = result.error;
  }
};
</script>

<template>
  <!-- Full-page auth layout with a gradient background -->
  <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-brand-50 to-blue-100 px-4">
    <div class="w-full max-w-md">

      <!-- Logo / Brand -->
      <div class="text-center mb-8">
        <div class="inline-flex items-center justify-center w-14 h-14 bg-brand-600 rounded-2xl shadow-lg mb-4">
          <i class="fas fa-tasks text-white text-2xl"></i>
        </div>
        <h1 class="font-display text-3xl font-bold text-gray-900">TaskFlow</h1>
        <p class="text-gray-500 mt-1 text-sm">Sign in to manage your tasks</p>
      </div>

      <!-- Login Card -->
      <div class="card p-8">
        <h2 class="text-xl font-semibold text-gray-900 mb-6">Welcome back</h2>

        <!-- General Error Alert -->
        <div v-if="errors.general" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2 text-red-700 text-sm">
          <i class="fas fa-exclamation-triangle"></i>
          {{ errors.general }}
        </div>

        <!-- Form — note: we use @submit.prevent to stop page reload -->
        <form @submit.prevent="handleLogin" novalidate class="space-y-4">

          <!-- Email Field -->
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
            <input
              v-model="form.email"
              type="email"
              placeholder="you@example.com"
              class="input-field"
              :class="{ 'border-red-400 ring-1 ring-red-400': errors.email }"
              autocomplete="email"
            />
            <p v-if="errors.email" class="mt-1 text-xs text-red-600">{{ errors.email }}</p>
          </div>

          <!-- Password Field with toggle visibility -->
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
            <div class="relative">
              <input
                v-model="form.password"
                :type="showPassword ? 'text' : 'password'"
                placeholder="Your password"
                class="input-field pr-10"
                :class="{ 'border-red-400 ring-1 ring-red-400': errors.password }"
                autocomplete="current-password"
              />
              <button
                type="button"
                @click="showPassword = !showPassword"
                class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
              >
                <i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" class="text-sm"></i>
              </button>
            </div>
            <p v-if="errors.password" class="mt-1 text-xs text-red-600">{{ errors.password }}</p>
          </div>

          <!-- Submit Button -->
          <button
            type="submit"
            class="btn-primary w-full justify-center py-2.5"
            :disabled="authStore.isLoading"
          >
            <i v-if="authStore.isLoading" class="fas fa-spinner fa-spin"></i>
            <i v-else class="fas fa-shield-alt"></i>
            {{ authStore.isLoading ? 'Signing in...' : 'Sign In' }}
          </button>
        </form>

        <!-- Register Link -->
        <p class="mt-6 text-center text-sm text-gray-500">
          Don't have an account?
          <router-link to="/register" class="text-brand-600 font-medium hover:text-brand-700">
            Create one
          </router-link>
        </p>
      </div>
    </div>
  </div>
</template>
```

### `client/src/views/RegisterView.vue`

```vue
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/authStore';

const router = useRouter();
const authStore = useAuthStore();

const form = reactive({ name: '', email: '', password: '', confirmPassword: '' });
const showPassword = ref(false);
const errors = reactive({ name: '', email: '', password: '', confirmPassword: '', general: '' });

const validateForm = () => {
  let valid = true;
  Object.keys(errors).forEach((k) => (errors[k] = ''));

  if (!form.name.trim()) { errors.name = 'Name is required'; valid = false; }
  else if (form.name.length < 2) { errors.name = 'Name must be at least 2 characters'; valid = false; }

  if (!form.email) { errors.email = 'Email is required'; valid = false; }
  else if (!/\S+@\S+\.\S+/.test(form.email)) { errors.email = 'Invalid email format'; valid = false; }

  if (!form.password) { errors.password = 'Password is required'; valid = false; }
  else if (form.password.length < 6) { errors.password = 'At least 6 characters required'; valid = false; }
  else if (!/\d/.test(form.password)) { errors.password = 'Must contain at least one number'; valid = false; }

  if (form.password !== form.confirmPassword) { errors.confirmPassword = 'Passwords do not match'; valid = false; }

  return valid;
};

const handleRegister = async () => {
  if (!validateForm()) return;
  const result = await authStore.register(form.name, form.email, form.password);
  if (result.success) router.push('/dashboard');
  else errors.general = result.error;
};
</script>

<template>
  <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-brand-50 to-blue-100 px-4 py-8">
    <div class="w-full max-w-md">
      <div class="text-center mb-8">
        <div class="inline-flex items-center justify-center w-14 h-14 bg-brand-600 rounded-2xl shadow-lg mb-4">
          <i class="fas fa-tasks text-white text-2xl"></i>
        </div>
        <h1 class="font-display text-3xl font-bold text-gray-900">TaskFlow</h1>
        <p class="text-gray-500 mt-1 text-sm">Create your free account</p>
      </div>

      <div class="card p-8">
        <h2 class="text-xl font-semibold text-gray-900 mb-6">Get started</h2>

        <div v-if="errors.general" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2 text-red-700 text-sm">
          <i class="fas fa-exclamation-triangle"></i>{{ errors.general }}
        </div>

        <form @submit.prevent="handleRegister" novalidate class="space-y-4">
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Full Name</label>
            <input v-model="form.name" type="text" placeholder="Jane Smith" class="input-field"
              :class="{ 'border-red-400': errors.name }" autocomplete="name"/>
            <p v-if="errors.name" class="mt-1 text-xs text-red-600">{{ errors.name }}</p>
          </div>

          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
            <input v-model="form.email" type="email" placeholder="you@example.com" class="input-field"
              :class="{ 'border-red-400': errors.email }" autocomplete="email"/>
            <p v-if="errors.email" class="mt-1 text-xs text-red-600">{{ errors.email }}</p>
          </div>

          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
            <div class="relative">
              <input v-model="form.password" :type="showPassword ? 'text' : 'password'"
                placeholder="Min. 6 chars + 1 number" class="input-field pr-10"
                :class="{ 'border-red-400': errors.password }" autocomplete="new-password"/>
              <button type="button" @click="showPassword = !showPassword"
                class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
                <i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" class="text-sm"></i>
              </button>
            </div>
            <p v-if="errors.password" class="mt-1 text-xs text-red-600">{{ errors.password }}</p>
          </div>

          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
            <input v-model="form.confirmPassword" type="password" placeholder="Repeat your password"
              class="input-field" :class="{ 'border-red-400': errors.confirmPassword }"
              autocomplete="new-password"/>
            <p v-if="errors.confirmPassword" class="mt-1 text-xs text-red-600">{{ errors.confirmPassword }}</p>
          </div>

          <button type="submit" class="btn-primary w-full justify-center py-2.5" :disabled="authStore.isLoading">
            <i v-if="authStore.isLoading" class="fas fa-spinner fa-spin"></i>
            <i v-else class="fas fa-user"></i>
            {{ authStore.isLoading ? 'Creating account...' : 'Create Account' }}
          </button>
        </form>

        <p class="mt-6 text-center text-sm text-gray-500">
          Already have an account?
          <router-link to="/login" class="text-brand-600 font-medium hover:text-brand-700">Sign in</router-link>
        </p>
      </div>
    </div>
  </div>
</template>
```

---

## Step 11: Dashboard & Task UI

### `client/src/components/Navbar.vue`

```vue
<script setup>
import { useAuthStore } from '../stores/authStore';
import { useRouter } from 'vue-router';

const authStore = useAuthStore();
const router = useRouter();

const handleLogout = () => {
  authStore.logout();
  router.push('/login');
};
</script>

<template>
  <nav class="bg-white border-b border-gray-200 sticky top-0 z-40">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="flex justify-between items-center h-16">

        <!-- Brand -->
        <div class="flex items-center gap-2">
          <div class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center">
            <i class="fas fa-tasks text-white text-sm"></i>
          </div>
          <span class="font-display font-bold text-gray-900 text-lg">TaskFlow</span>
        </div>

        <!-- User Menu -->
        <div class="flex items-center gap-3">
          <!-- User Avatar -->
          <div class="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-full">
            <div class="w-6 h-6 bg-brand-600 rounded-full flex items-center justify-center text-white text-xs font-semibold">
              {{ authStore.userInitials }}
            </div>
            <span class="text-sm text-gray-700 font-medium hidden sm:inline">
              {{ authStore.userName }}
            </span>
          </div>

          <!-- Logout Button -->
          <button @click="handleLogout" class="btn-secondary text-xs px-3 py-1.5">
            <i class="fas fa-sign-out-alt"></i>
            <span class="hidden sm:inline">Logout</span>
          </button>
        </div>

      </div>
    </div>
  </nav>
</template>
```

### `client/src/components/TaskCard.vue`

```vue
<script setup>
import { computed } from 'vue';

const props = defineProps({
  task: { type: Object, required: true },
});

const emit = defineEmits(['edit', 'delete', 'toggle-status']);

// Computed badge classes based on task properties
const statusClass = computed(() => ({
  'todo': 'badge-todo',
  'in-progress': 'badge-progress',
  'done': 'badge-done',
}[props.task.status]));

const priorityClass = computed(() => ({
  'low': 'badge-low',
  'medium': 'badge-medium',
  'high': 'badge-high',
}[props.task.priority]));

const statusLabel = computed(() => ({
  'todo': 'To Do',
  'in-progress': 'In Progress',
  'done': 'Done',
}[props.task.status]));

const formattedDueDate = computed(() => {
  if (!props.task.dueDate) return null;
  return new Date(props.task.dueDate).toLocaleDateString('en-US', {
    month: 'short', day: 'numeric', year: 'numeric',
  });
});

const isOverdue = computed(() => {
  if (!props.task.dueDate || props.task.status === 'done') return false;
  return new Date() > new Date(props.task.dueDate);
});

// Cycle through statuses on quick-toggle
const nextStatus = computed(() => ({
  'todo': 'in-progress',
  'in-progress': 'done',
  'done': 'todo',
}[props.task.status]));
</script>

<template>
  <div
    class="card p-4 hover:shadow-md transition-shadow duration-200"
    :class="{ 'opacity-60': task.status === 'done' }"
  >
    <!-- Header Row -->
    <div class="flex items-start justify-between gap-2 mb-3">
      <h3
        class="font-medium text-gray-900 text-sm leading-snug line-clamp-2"
        :class="{ 'line-through text-gray-500': task.status === 'done' }"
      >
        {{ task.title }}
      </h3>

      <!-- Quick Actions -->
      <div class="flex items-center gap-1 shrink-0">
        <button
          @click="emit('toggle-status', task._id, nextStatus)"
          class="p-1.5 text-gray-400 hover:text-brand-600 hover:bg-brand-50 rounded-md transition-colors"
          :title="`Mark as ${nextStatus}`"
        >
          <i :class="task.status === 'done' ? 'fas fa-circle' : 'fas fa-check-circle'" class="text-xs"></i>
        </button>
        <button @click="emit('edit', task)"
          class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors">
          <i class="fas fa-edit text-xs"></i>
        </button>
        <button @click="emit('delete', task._id)"
          class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors">
          <i class="fas fa-trash text-xs"></i>
        </button>
      </div>
    </div>

    <!-- Description -->
    <p v-if="task.description" class="text-xs text-gray-500 mb-3 line-clamp-2">
      {{ task.description }}
    </p>

    <!-- Badges Row -->
    <div class="flex flex-wrap gap-1.5 mb-3">
      <span :class="statusClass">{{ statusLabel }}</span>
      <span :class="priorityClass">
        <i class="fas fa-tag mr-0.5"></i>{{ task.priority }}
      </span>
    </div>

    <!-- Footer: Due Date + Tags -->
    <div class="flex items-center justify-between">
      <div v-if="formattedDueDate"
        class="flex items-center gap-1 text-xs"
        :class="isOverdue ? 'text-red-600 font-medium' : 'text-gray-400'"
      >
        <i class="fas fa-calendar text-xs"></i>
        {{ formattedDueDate }}
        <span v-if="isOverdue" class="font-semibold">(Overdue)</span>
      </div>

      <div class="flex flex-wrap gap-1">
        <span v-for="tag in task.tags.slice(0, 2)" :key="tag"
          class="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded">
          {{ tag }}
        </span>
        <span v-if="task.tags.length > 2" class="text-xs text-gray-400">
          +{{ task.tags.length - 2 }}
        </span>
      </div>
    </div>
  </div>
</template>
```

### `client/src/components/TaskModal.vue`

```vue
<script setup>
import { ref, reactive, watch } from 'vue';

const props = defineProps({
  isOpen: Boolean,
  task: { type: Object, default: null }, // null = create mode, object = edit mode
});

const emit = defineEmits(['close', 'save']);

// Form state initialized reactively
const form = reactive({
  title: '', description: '', status: 'todo',
  priority: 'medium', dueDate: '', tags: '',
});

const errors = reactive({ title: '' });

// Watch for when the modal opens — pre-fill if editing
watch(() => props.task, (newTask) => {
  if (newTask) {
    form.title = newTask.title;
    form.description = newTask.description || '';
    form.status = newTask.status;
    form.priority = newTask.priority;
    form.dueDate = newTask.dueDate ? newTask.dueDate.substring(0, 10) : '';
    form.tags = (newTask.tags || []).join(', ');
  } else {
    // Reset form for create mode
    Object.assign(form, { title: '', description: '', status: 'todo',
      priority: 'medium', dueDate: '', tags: '' });
  }
  errors.title = '';
}, { immediate: true });

const handleSave = () => {
  errors.title = '';
  if (!form.title.trim()) { errors.title = 'Title is required'; return; }

  const taskData = {
    title: form.title.trim(),
    description: form.description.trim(),
    status: form.status,
    priority: form.priority,
    dueDate: form.dueDate || null,
    tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean),
  };

  emit('save', taskData);
};
</script>

<template>
  <!-- Teleport modal to body to avoid z-index issues -->
  <Teleport to="body">
    <!-- Backdrop -->
    <Transition name="fade">
      <div v-if="isOpen" class="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4"
        @click.self="emit('close')">

        <!-- Modal Panel -->
        <Transition name="slide-up">
          <div v-if="isOpen" class="bg-white rounded-2xl shadow-xl w-full max-w-lg">

            <!-- Header -->
            <div class="flex items-center justify-between p-6 border-b border-gray-100">
              <h2 class="font-semibold text-gray-900 text-lg">
                {{ task ? 'Edit Task' : 'New Task' }}
              </h2>
              <button @click="emit('close')"
                class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
                <i class="fas fa-times"></i>
              </button>
            </div>

            <!-- Body -->
            <div class="p-6 space-y-4">

              <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Title *</label>
                <input v-model="form.title" type="text" placeholder="What needs to be done?"
                  class="input-field" :class="{ 'border-red-400': errors.title }"/>
                <p v-if="errors.title" class="mt-1 text-xs text-red-600">{{ errors.title }}</p>
              </div>

              <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
                <textarea v-model="form.description" rows="3" placeholder="Add more details..."
                  class="input-field resize-none"></textarea>
              </div>

              <div class="grid grid-cols-2 gap-3">
                <div>
                  <label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
                  <select v-model="form.status" class="input-field">
                    <option value="todo">To Do</option>
                    <option value="in-progress">In Progress</option>
                    <option value="done">Done</option>
                  </select>
                </div>
                <div>
                  <label class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
                  <select v-model="form.priority" class="input-field">
                    <option value="low">Low</option>
                    <option value="medium">Medium</option>
                    <option value="high">High</option>
                  </select>
                </div>
              </div>

              <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Due Date</label>
                <input v-model="form.dueDate" type="date" class="input-field"/>
              </div>

              <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">Tags</label>
                <input v-model="form.tags" type="text" placeholder="work, urgent, design (comma separated)"
                  class="input-field"/>
              </div>
            </div>

            <!-- Footer -->
            <div class="flex items-center justify-end gap-2 p-6 border-t border-gray-100">
              <button @click="emit('close')" class="btn-secondary">Cancel</button>
              <button @click="handleSave" class="btn-primary">
                <i :class="task ? 'fas fa-check' : 'fas fa-plus'"></i>
                {{ task ? 'Save Changes' : 'Create Task' }}
              </button>
            </div>

          </div>
        </Transition>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.2s ease, opacity 0.2s ease; }
.slide-up-enter-from, .slide-up-leave-to { transform: translateY(16px); opacity: 0; }
</style>
```

### `client/src/views/DashboardView.vue`

```vue
<script setup>
import { ref, onMounted } from 'vue';
import { useTaskStore } from '../stores/taskStore';
import Navbar from '../components/Navbar.vue';
import TaskCard from '../components/TaskCard.vue';
import TaskModal from '../components/TaskModal.vue';

const taskStore = useTaskStore();

// Modal state
const isModalOpen = ref(false);
const editingTask = ref(null);

const openCreate = () => { editingTask.value = null; isModalOpen.value = true; };
const openEdit = (task) => { editingTask.value = task; isModalOpen.value = true; };
const closeModal = () => { isModalOpen.value = false; editingTask.value = null; };

// Save handler — works for both create and edit
const handleSave = async (taskData) => {
  let result;
  if (editingTask.value) {
    result = await taskStore.updateTask(editingTask.value._id, taskData);
  } else {
    result = await taskStore.createTask(taskData);
  }
  if (result.success) closeModal();
};

const handleDelete = async (id) => {
  if (confirm('Delete this task?')) await taskStore.deleteTask(id);
};

const handleToggleStatus = async (id, newStatus) => {
  await taskStore.updateTask(id, { status: newStatus });
};

// Load tasks and stats when dashboard mounts
onMounted(async () => {
  await Promise.all([taskStore.fetchTasks(), taskStore.fetchStats()]);
});
</script>

<template>
  <div class="min-h-screen bg-gray-50">
    <Navbar />

    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

      <!-- Header -->
      <div class="flex items-center justify-between mb-8">
        <div>
          <h1 class="font-display text-2xl font-bold text-gray-900">My Tasks</h1>
          <p class="text-gray-500 text-sm mt-0.5">{{ taskStore.stats.total }} tasks total</p>
        </div>
        <button @click="openCreate" class="btn-primary">
          <i class="fas fa-plus"></i> New Task
        </button>
      </div>

      <!-- Stats Cards -->
      <div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
        <div class="card p-4">
          <p class="text-xs text-gray-500 mb-1">Total</p>
          <p class="text-2xl font-bold text-gray-900">{{ taskStore.stats.total }}</p>
        </div>
        <div class="card p-4">
          <p class="text-xs text-gray-500 mb-1">To Do</p>
          <p class="text-2xl font-bold text-gray-600">{{ taskStore.stats.todo || 0 }}</p>
        </div>
        <div class="card p-4">
          <p class="text-xs text-gray-500 mb-1">In Progress</p>
          <p class="text-2xl font-bold text-blue-600">{{ taskStore.stats['in-progress'] || 0 }}</p>
        </div>
        <div class="card p-4">
          <p class="text-xs text-gray-500 mb-1">Done</p>
          <p class="text-2xl font-bold text-green-600">{{ taskStore.stats.done || 0 }}</p>
        </div>
      </div>

      <!-- Filters Bar -->
      <div class="card p-4 mb-6 flex flex-wrap gap-3 items-center">
        <div class="flex items-center gap-2 flex-1 min-w-48">
          <i class="fas fa-search text-gray-400 text-sm"></i>
          <input
            :value="taskStore.filters.search"
            @input="taskStore.setFilter('search', $event.target.value)"
            type="text" placeholder="Search tasks..."
            class="flex-1 text-sm outline-none bg-transparent placeholder-gray-400"
          />
        </div>
        <select
          :value="taskStore.filters.status"
          @change="taskStore.setFilter('status', $event.target.value)"
          class="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-brand-500"
        >
          <option value="">All Status</option>
          <option value="todo">To Do</option>
          <option value="in-progress">In Progress</option>
          <option value="done">Done</option>
        </select>
        <select
          :value="taskStore.filters.priority"
          @change="taskStore.setFilter('priority', $event.target.value)"
          class="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-brand-500"
        >
          <option value="">All Priorities</option>
          <option value="low">Low</option>
          <option value="medium">Medium</option>
          <option value="high">High</option>
        </select>
        <button v-if="taskStore.filters.status || taskStore.filters.priority || taskStore.filters.search"
          @click="taskStore.clearFilters()"
          class="text-xs text-gray-500 hover:text-gray-700 flex items-center gap-1">
          <i class="fas fa-times"></i> Clear
        </button>
      </div>

      <!-- Overdue Alert -->
      <div v-if="taskStore.overdueTasks.length > 0"
        class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl flex items-center gap-3 text-red-700">
        <i class="fas fa-exclamation-triangle"></i>
        <span class="text-sm font-medium">
          {{ taskStore.overdueTasks.length }} task{{ taskStore.overdueTasks.length > 1 ? 's are' : ' is' }} overdue!
        </span>
      </div>

      <!-- Loading State -->
      <div v-if="taskStore.isLoading" class="flex justify-center py-16">
        <div class="flex items-center gap-3 text-gray-500">
          <i class="fas fa-spinner fa-spin text-brand-600"></i>
          <span class="text-sm">Loading tasks...</span>
        </div>
      </div>

      <!-- Empty State -->
      <div v-else-if="taskStore.tasks.length === 0" class="text-center py-20">
        <div class="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
          <i class="fas fa-tasks text-gray-400 text-2xl"></i>
        </div>
        <h3 class="font-semibold text-gray-900 mb-1">No tasks yet</h3>
        <p class="text-gray-500 text-sm mb-4">Create your first task to get started.</p>
        <button @click="openCreate" class="btn-primary">
          <i class="fas fa-plus"></i> Create Task
        </button>
      </div>

      <!-- Task Grid -->
      <div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
        <TaskCard
          v-for="task in taskStore.tasks"
          :key="task._id"
          :task="task"
          @edit="openEdit"
          @delete="handleDelete"
          @toggle-status="handleToggleStatus"
        />
      </div>

    </main>

    <!-- Task Modal -->
    <TaskModal
      :is-open="isModalOpen"
      :task="editingTask"
      @close="closeModal"
      @save="handleSave"
    />
  </div>
</template>
```

### `client/src/App.vue`

```vue
<!-- App.vue — Root component. RouterView renders the current page's component. -->
<template>
  <RouterView />
</template>
```

### `client/src/.env`

```env
# Frontend environment variables (Vite requires VITE_ prefix)
VITE_API_URL=http://localhost:5000/api
```

---

## Step 12: Running the Project

### Install & Run

```bash
# ─── Option 1: Run separately ──────────────────────────────────────────────

# Terminal 1 — Backend
cd taskflow/server
npm run dev
# Server will start on http://localhost:5000

# Terminal 2 — Frontend
cd taskflow/client
npm run dev
# Vue app will start on http://localhost:5173

# ─── Option 2: Concurrent (recommended) ────────────────────────────────────

# Install concurrently in the root
cd taskflow
npm init -y
npm install concurrently --save-dev
```

Create `taskflow/package.json`:

```json
{
  "name": "taskflow",
  "scripts": {
    "dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
    "install:all": "npm install --prefix server && npm install --prefix client"
  },
  "devDependencies": {
    "concurrently": "^8.0.0"
  }
}
```

```bash
# From taskflow/ root
npm run install:all   # Install all dependencies
npm run dev           # Start both servers concurrently
```

### Testing the API with curl

```bash
# Register a user
curl -X POST http://localhost:5000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane Smith","email":"jane@example.com","password":"password123"}'

# Login
curl -X POST http://localhost:5000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","password":"password123"}'

# Create a task (replace TOKEN with the accessToken from login response)
curl -X POST http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -d '{"title":"Build MEVN app","priority":"high","status":"in-progress"}'

# Get all tasks
curl http://localhost:5000/api/tasks \
  -H "Authorization: Bearer TOKEN"
```

---

## Step 13: Summary, Extensions & Best Practices

### What You've Built

| Layer | Technology | Purpose |
|-------|-----------|---------|
| Database | MongoDB + Mongoose | Persistent storage, schema validation, transactions |
| API Server | Node.js + Express | RESTful API, routing, middleware chain |
| Auth | bcryptjs + JWT | Secure password hashing + stateless auth |
| Validation | express-validator | Input sanitization and validation |
| State | Pinia | Centralized reactive state management |
| Routing | Vue Router | SPA navigation + auth guards |
| Styling | Tailwind CSS | Utility-first responsive UI |
| HTTP | Axios | API calls + automatic JWT header injection |

### How to Extend This Project

**1. File Uploads (avatars / task attachments)**
```bash
npm install multer cloudinary
```
Create an `uploadController.js` that handles multipart forms and stores files in Cloudinary.

**2. Real-time notifications with Socket.IO**
```bash
npm install socket.io
# client: npm install socket.io-client
```
Emit events when tasks change — other users see updates instantly.

**3. Email notifications (nodemailer + due date reminders)**
```bash
npm install nodemailer node-cron
```
Use node-cron to run a job at 9am daily, checking for tasks due today.

**4. Role-based access control (admin / member)**
Add a `role` field to the User model and check it in middleware:
```javascript
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    res.status(403); throw new Error('Forbidden');
  }
  next();
};
```

**5. Unit Testing**
```bash
npm install --save-dev jest supertest @jest/globals
```
Write tests for controllers using `supertest` to simulate HTTP requests.

**6. TypeScript**
Add `ts-node`, `typescript`, and type definitions. Convert `.js` → `.ts` gradually.

**7. Deployment**
- **Backend**: Deploy to Railway, Render, or a VPS with PM2
- **Frontend**: Deploy to Vercel or Netlify (it's a static SPA)
- **Database**: Use MongoDB Atlas (free tier) instead of local MongoDB

### Production Best Practices Checklist

- [ ] Use HTTPS in production (Let's Encrypt / managed TLS)
- [ ] Set strong, long JWT secrets (use `openssl rand -hex 64`)
- [ ] Use MongoDB Atlas with IP whitelisting
- [ ] Add rate limiting (`npm install express-rate-limit`)
- [ ] Add helmet CSP headers
- [ ] Never log passwords or tokens
- [ ] Use PM2 for process management in production
- [ ] Set `NODE_ENV=production` to disable error stack traces
- [ ] Add database backups (MongoDB Atlas has this built-in)
- [ ] Implement refresh token rotation (invalidate old refresh tokens on use)

### Key Concepts Reference

```
MEVN Flow:
  Browser → Vue Router → Pinia Action → Axios (+ JWT)
         → Express Route → Auth Middleware → Controller
         → Mongoose → MongoDB
         → JSON Response → Pinia State update → Vue re-render

JWT Flow:
  Register/Login → bcrypt verify → sign(payload, secret) → return token
  Protected request → Bearer token → jwt.verify() → attach req.user → next()

Transaction Flow:
  startSession() → startTransaction() → all DB ops → commitTransaction()
  if error → abortTransaction() → finally endSession()

Pinia (defineStore):
  ref() = reactive state
  computed() = derived/getter
  function = action (can be async)
```

---

*Happy coding! The best next step is to build it, break it, and fix it. Every bug is a lesson.*
