# 📚 คู่มือสร้าง Simple Blog: จาก Express.js สู่ Clean Architecture และ Event-Driven

**เป้าหมาย:** สร้าง Blog System ที่มีฟีเจอร์พื้นฐาน แล้ว refactor ผ่าน 3 architecture patterns

**ระยะเวลาโดยประมาณ:** 8-12 ชั่วโมง

> 📝 **คำอธิบาย:** คู่มือนี้จะสอนวิธีการพัฒนา Blog Application ผ่าน 3 ขั้นตอน เริ่มจากโครงสร้างแบบเรียบง่าย ไปจนถึงการใช้ Event-Driven Architecture ซึ่งเป็นรูปแบบที่ใช้ในแอปพลิเคชันขนาดใหญ่

---

## 📋 Table of Contents

1. [Phase 1: Simple Express.js (MVC-like)](#phase-1-simple-expressjs-mvc-like)
2. [Phase 2: Refactor to Clean Architecture](#phase-2-refactor-to-clean-architecture)
3. [Phase 3: Add Event-Driven with Redis](#phase-3-add-event-driven-with-redis)

---

## 🎯 ฟีเจอร์ที่จะสร้าง

- ✅ CRUD Posts (Create, Read, Update, Delete)
- ✅ CRUD Comments
- ✅ User Authentication (Simple)
- ✅ View Counter
- ✅ Events: Post Created, Comment Added

---

## 🛠️ เตรียมความพร้อม

### ติดตั้ง Prerequisites

```bash
# ตรวจสอบ Node.js
node --version  # ต้อง v14 ขึ้นไป

# ติดตั้ง Docker (สำหรับ Phase 3)
# Download: https://www.docker.com/products/docker-desktop
```

> 🔧 **คำอธิบาย:** Node.js เป็นรันไทม์สำหรับ JavaScript บนเซิร์ฟเวอร์ Docker ใช้สำหรับรัน Redis ซึ่งเป็นดาตาเบสแบบ In-Memory ที่เร็วมาก

### สร้างโฟลเดอร์โปรเจกต์

```bash
mkdir blog-evolution
cd blog-evolution
```

---

# Phase 1: Simple Express.js (MVC-like)

> 📖 **Phase 1 คำอธิบาย:**
>
> - เป็นขั้นตอนแรกที่เราสร้าง Blog ด้วยวิธีที่ง่ายและรวดเร็ว
> - ใช้ MVC pattern (Model-View-Controller) ที่เรียบง่าย
> - Models: ความหมายของข้อมูล (Post, User, Comment)
> - Controllers: ลอจิกที่จัดการ HTTP requests
> - Routes: URL paths ที่เชื่อมไปยัง Controllers
> - ข้อจำกัด: Code coupling สูง ยากต่อการ test และขยายในอนาคต

## 📁 โครงสร้างโปรเจกต์

```
blog-phase1/
├── node_modules/
├── src/
│   ├── models/
│   │   ├── Post.js
│   │   ├── Comment.js
│   │   └── User.js
│   ├── controllers/
│   │   ├── postController.js
│   │   ├── commentController.js
│   │   └── authController.js
│   ├── routes/
│   │   ├── posts.js
│   │   ├── comments.js
│   │   └── auth.js
│   ├── middleware/
│   │   └── auth.js
│   ├── config/
│   │   └── database.js
│   └── app.js
├── package.json
└── server.js
```

---

## 🚀 Step 1: Setup โปรเจกต์

```bash
# สร้างโฟลเดอร์
mkdir blog-phase1
cd blog-phase1

# Initialize npm
npm init -y

# ติดตั้ง dependencies
npm install express sqlite3 bcryptjs jsonwebtoken dotenv
npm install --save-dev nodemon
```

### แก้ไข `package.json`

```json
{
  "name": "blog-phase1",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "sqlite3": "^5.1.6",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.2",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}
```

### สร้าง `.env`

```env
PORT=3000
JWT_SECRET=your_super_secret_key_change_this
DB_PATH=./database.sqlite
```

---

## 🗄️ Step 2: Database Configuration

### `src/config/database.js`

```javascript
const sqlite3 = require("sqlite3").verbose();
const path = require("path");

const dbPath = process.env.DB_PATH || "./database.sqlite";

const db = new sqlite3.Database(dbPath, (err) => {
  if (err) {
    console.error("Database connection error:", err);
  } else {
    console.log("Connected to SQLite database");
    initDatabase();
  }
});

function initDatabase() {
  // Users table
  db.run(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT UNIQUE NOT NULL,
      email TEXT UNIQUE NOT NULL,
      password TEXT NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // Posts table
  db.run(`
    CREATE TABLE IF NOT EXISTS posts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      content TEXT NOT NULL,
      author_id INTEGER NOT NULL,
      view_count INTEGER DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (author_id) REFERENCES users(id)
    )
  `);

  // Comments table
  db.run(`
    CREATE TABLE IF NOT EXISTS comments (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      post_id INTEGER NOT NULL,
      user_id INTEGER NOT NULL,
      content TEXT NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (post_id) REFERENCES posts(id),
      FOREIGN KEY (user_id) REFERENCES users(id)
    )
  `);
}

module.exports = db;
```

---

## 📦 Step 3: Models

> 💾 **Models คำอธิบาย:**
>
> - ตัวแทนของข้อมูลในระบบ
> - ส่วนที่เชื่อมต่อกับดาตาเบส
> - ประกอบด้วย methods สำหรับ CRUD operations
> - User: เก็บข้อมูลผู้ใช้ (ชื่อ อีเมล รหัสผ่าน)
> - Post: เก็บบทความ
> - Comment: เก็บความเห็นของผู้ใช้บนบทความ

### `src/models/User.js`

```javascript
const db = require("../config/database");
const bcrypt = require("bcryptjs");

class User {
  static async create(username, email, password) {
    const hashedPassword = await bcrypt.hash(password, 10);

    return new Promise((resolve, reject) => {
      db.run(
        "INSERT INTO users (username, email, password) VALUES (?, ?, ?)",
        [username, email, hashedPassword],
        function (err) {
          if (err) reject(err);
          else resolve({ id: this.lastID, username, email });
        },
      );
    });
  }

  static async findByEmail(email) {
    return new Promise((resolve, reject) => {
      db.get("SELECT * FROM users WHERE email = ?", [email], (err, row) => {
        if (err) reject(err);
        else resolve(row);
      });
    });
  }

  static async findById(id) {
    return new Promise((resolve, reject) => {
      db.get(
        "SELECT id, username, email FROM users WHERE id = ?",
        [id],
        (err, row) => {
          if (err) reject(err);
          else resolve(row);
        },
      );
    });
  }

  static async comparePassword(plainPassword, hashedPassword) {
    return bcrypt.compare(plainPassword, hashedPassword);
  }
}

module.exports = User;
```

### `src/models/Post.js`

```javascript
const db = require("../config/database");

class Post {
  static async create(title, content, authorId) {
    return new Promise((resolve, reject) => {
      db.run(
        "INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)",
        [title, content, authorId],
        function (err) {
          if (err) reject(err);
          else
            resolve({ id: this.lastID, title, content, author_id: authorId });
        },
      );
    });
  }

  static async findAll() {
    return new Promise((resolve, reject) => {
      db.all(
        `SELECT posts.*, users.username as author_name 
         FROM posts 
         JOIN users ON posts.author_id = users.id 
         ORDER BY posts.created_at DESC`,
        [],
        (err, rows) => {
          if (err) reject(err);
          else resolve(rows);
        },
      );
    });
  }

  static async findById(id) {
    return new Promise((resolve, reject) => {
      db.get(
        `SELECT posts.*, users.username as author_name 
         FROM posts 
         JOIN users ON posts.author_id = users.id 
         WHERE posts.id = ?`,
        [id],
        (err, row) => {
          if (err) reject(err);
          else resolve(row);
        },
      );
    });
  }

  static async update(id, title, content) {
    return new Promise((resolve, reject) => {
      db.run(
        "UPDATE posts SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
        [title, content, id],
        function (err) {
          if (err) reject(err);
          else resolve({ id, title, content });
        },
      );
    });
  }

  static async delete(id) {
    return new Promise((resolve, reject) => {
      db.run("DELETE FROM posts WHERE id = ?", [id], function (err) {
        if (err) reject(err);
        else resolve({ deleted: this.changes > 0 });
      });
    });
  }

  static async incrementViewCount(id) {
    return new Promise((resolve, reject) => {
      db.run(
        "UPDATE posts SET view_count = view_count + 1 WHERE id = ?",
        [id],
        function (err) {
          if (err) reject(err);
          else resolve({ success: true });
        },
      );
    });
  }
}

module.exports = Post;
```

### `src/models/Comment.js`

```javascript
const db = require("../config/database");

class Comment {
  static async create(postId, userId, content) {
    return new Promise((resolve, reject) => {
      db.run(
        "INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)",
        [postId, userId, content],
        function (err) {
          if (err) reject(err);
          else
            resolve({
              id: this.lastID,
              post_id: postId,
              user_id: userId,
              content,
            });
        },
      );
    });
  }

  static async findByPostId(postId) {
    return new Promise((resolve, reject) => {
      db.all(
        `SELECT comments.*, users.username 
         FROM comments 
         JOIN users ON comments.user_id = users.id 
         WHERE comments.post_id = ? 
         ORDER BY comments.created_at DESC`,
        [postId],
        (err, rows) => {
          if (err) reject(err);
          else resolve(rows);
        },
      );
    });
  }

  static async delete(id, userId) {
    return new Promise((resolve, reject) => {
      db.run(
        "DELETE FROM comments WHERE id = ? AND user_id = ?",
        [id, userId],
        function (err) {
          if (err) reject(err);
          else resolve({ deleted: this.changes > 0 });
        },
      );
    });
  }
}

module.exports = Comment;
```

---

## 🎮 Step 4: Controllers

> 🎛️ **Controllers คำอธิบาย:**
>
> - ตัวจัดการ HTTP requests จากไคลเอนต์
> - ประมวลผลข้อมูลและเรียก Models เพื่อบันทึก/อ่านข้อมูล
> - ส่งคำตอบกลับไปยังไคลเอนต์ (responses)
> - authController: จัดการการลงทะเบียน login
> - postController: จัดการบทความ (CRUD)
> - commentController: จัดการความเห็น

### `src/controllers/authController.js`

```javascript
const User = require("../models/User");
const jwt = require("jsonwebtoken");

exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;

    // Validation
    if (!username || !email || !password) {
      return res.status(400).json({ error: "All fields are required" });
    }

    // Check if user exists
    const existingUser = await User.findByEmail(email);
    if (existingUser) {
      return res.status(400).json({ error: "Email already registered" });
    }

    // Create user
    const user = await User.create(username, email, password);

    // Generate token
    const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
      expiresIn: "7d",
    });

    res.status(201).json({
      message: "User registered successfully",
      user: { id: user.id, username: user.username, email: user.email },
      token,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Find user
    const user = await User.findByEmail(email);
    if (!user) {
      return res.status(401).json({ error: "Invalid credentials" });
    }

    // Check password
    const isValidPassword = await User.comparePassword(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ error: "Invalid credentials" });
    }

    // Generate token
    const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
      expiresIn: "7d",
    });

    res.json({
      message: "Login successful",
      user: { id: user.id, username: user.username, email: user.email },
      token,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};
```

### `src/controllers/postController.js`

```javascript
const Post = require("../models/Post");

exports.getAllPosts = async (req, res) => {
  try {
    const posts = await Post.findAll();
    res.json({ posts });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.getPost = async (req, res) => {
  try {
    const post = await Post.findById(req.params.id);

    if (!post) {
      return res.status(404).json({ error: "Post not found" });
    }

    // Increment view count
    await Post.incrementViewCount(post.id);

    res.json({ post });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.createPost = async (req, res) => {
  try {
    const { title, content } = req.body;
    const authorId = req.user.id;

    if (!title || !content) {
      return res.status(400).json({ error: "Title and content are required" });
    }

    const post = await Post.create(title, content, authorId);

    res.status(201).json({
      message: "Post created successfully",
      post,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.updatePost = async (req, res) => {
  try {
    const { title, content } = req.body;
    const postId = req.params.id;

    // Check if post exists and user owns it
    const post = await Post.findById(postId);
    if (!post) {
      return res.status(404).json({ error: "Post not found" });
    }

    if (post.author_id !== req.user.id) {
      return res.status(403).json({ error: "Not authorized" });
    }

    const updatedPost = await Post.update(postId, title, content);

    res.json({
      message: "Post updated successfully",
      post: updatedPost,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.deletePost = async (req, res) => {
  try {
    const postId = req.params.id;

    // Check if post exists and user owns it
    const post = await Post.findById(postId);
    if (!post) {
      return res.status(404).json({ error: "Post not found" });
    }

    if (post.author_id !== req.user.id) {
      return res.status(403).json({ error: "Not authorized" });
    }

    await Post.delete(postId);

    res.json({ message: "Post deleted successfully" });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};
```

### `src/controllers/commentController.js`

```javascript
const Comment = require("../models/Comment");
const Post = require("../models/Post");

exports.getComments = async (req, res) => {
  try {
    const postId = req.params.postId;
    const comments = await Comment.findByPostId(postId);
    res.json({ comments });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.createComment = async (req, res) => {
  try {
    const { content } = req.body;
    const postId = req.params.postId;
    const userId = req.user.id;

    // Check if post exists
    const post = await Post.findById(postId);
    if (!post) {
      return res.status(404).json({ error: "Post not found" });
    }

    if (!content) {
      return res.status(400).json({ error: "Content is required" });
    }

    const comment = await Comment.create(postId, userId, content);

    res.status(201).json({
      message: "Comment created successfully",
      comment,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.deleteComment = async (req, res) => {
  try {
    const commentId = req.params.id;
    const userId = req.user.id;

    const result = await Comment.delete(commentId, userId);

    if (!result.deleted) {
      return res
        .status(404)
        .json({ error: "Comment not found or not authorized" });
    }

    res.json({ message: "Comment deleted successfully" });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};
```

---

## 🔐 Step 5: Middleware

### `src/middleware/auth.js`

```javascript
const jwt = require("jsonwebtoken");
const User = require("../models/User");

async function authMiddleware(req, res, next) {
  try {
    // Get token from header
    const token = req.headers.authorization?.split(" ")[1];

    if (!token) {
      return res.status(401).json({ error: "Authentication required" });
    }

    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Get user
    const user = await User.findById(decoded.id);

    if (!user) {
      return res.status(401).json({ error: "User not found" });
    }

    // Attach user to request
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({ error: "Invalid token" });
  }
}

module.exports = authMiddleware;
```

---

## 🛣️ Step 6: Routes

### `src/routes/auth.js`

```javascript
const express = require("express");
const router = express.Router();
const authController = require("../controllers/authController");

router.post("/register", authController.register);
router.post("/login", authController.login);

module.exports = router;
```

### `src/routes/posts.js`

```javascript
const express = require("express");
const router = express.Router();
const postController = require("../controllers/postController");
const authMiddleware = require("../middleware/auth");

router.get("/", postController.getAllPosts);
router.get("/:id", postController.getPost);
router.post("/", authMiddleware, postController.createPost);
router.put("/:id", authMiddleware, postController.updatePost);
router.delete("/:id", authMiddleware, postController.deletePost);

module.exports = router;
```

### `src/routes/comments.js`

```javascript
const express = require("express");
const router = express.Router();
const commentController = require("../controllers/commentController");
const authMiddleware = require("../middleware/auth");

router.get("/post/:postId", commentController.getComments);
router.post("/post/:postId", authMiddleware, commentController.createComment);
router.delete("/:id", authMiddleware, commentController.deleteComment);

module.exports = router;
```

---

## 🏗️ Step 7: Main Application

### `src/app.js`

```javascript
const express = require("express");
require("dotenv").config();

const authRoutes = require("./routes/auth");
const postRoutes = require("./routes/posts");
const commentRoutes = require("./routes/comments");

const app = express();

// Middleware
app.use(express.json());

// Routes
app.use("/api/auth", authRoutes);
app.use("/api/posts", postRoutes);
app.use("/api/comments", commentRoutes);

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok", message: "Blog API Phase 1" });
});

// Error handling
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Something went wrong!" });
});

module.exports = app;
```

### `server.js`

```javascript
const app = require("./src/app");

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
  console.log(`📝 Phase 1: Simple Express.js (MVC-like)`);
});
```

---

## ✅ Step 8: ทดสอบ API

### รันโปรเจกต์

```bash
npm run dev
```

### ทดสอบด้วย curl หรือ Postman

```bash
# 1. Register user
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john",
    "email": "john@example.com",
    "password": "password123"
  }'

# 2. Login
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "password": "password123"
  }'

# เก็บ token ที่ได้จากการ login
TOKEN="your_token_here"

# 3. Create post
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "My First Post",
    "content": "This is my first blog post!"
  }'

# 4. Get all posts
curl http://localhost:3000/api/posts

# 5. Add comment
curl -X POST http://localhost:3000/api/comments/post/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "content": "Great post!"
  }'
```

---

## 📊 สรุป Phase 1

✅ **สิ่งที่ได้:**

- โครงสร้างแบบ MVC พื้นฐาน
- Authentication ด้วย JWT
- CRUD Posts และ Comments
- Database ด้วย SQLite

> 📝 **สรุปข้อสำคัญ Phase 1:**
>
> - Pros: เร็ว เข้าใจง่าย ดีสำหรับโปรเจกต์เล็ก
> - Cons: Models ผูกกับ Database โดยตรง ยากต่อ testing ยากต่อการขยาย

❌ **ข้อจำกัด:**

- Code coupling สูง (Models ผูกกับ Database โดยตรง)
- ยาก test (ต้อง mock database)
- Business logic กระจายอยู่ใน Controllers
- ไม่มี Event system

---

# Phase 2: Refactor to Clean Architecture

> 🏛️ **Phase 2 คำอธิบาย:**
>
> - Clean Architecture (Hexagonal/Onion) ออกแบบให้ business logic ไม่ขึ้นต่อ framework หรือ database
> - Core Layer: ตรงกลาง มี pure business logic ไม่ขึ้นต่อ external tools
> - Adapters Layer: ชั้นกลาง จัดการ HTTP requests และ database operations
> - Infrastructure Layer: ชั้นนอก มี tools อย่าง Express, SQLite, Redis
> - Dependency Flow: หมุนเข้าหาชั้นใน (ไม่ได้ออกนอก)
> - Benefit: ง่าย test เพลี่ยเปลี่ยน framework/database ได้ง่าย business logic ชัดเจน

## 📁 โครงสร้างใหม่

```
blog-phase2/
├── src/
│   ├── core/                    # ชั้นในสุด - Pure Business Logic
│   │   ├── entities/
│   │   │   ├── Post.js
│   │   │   ├── Comment.js
│   │   │   └── User.js
│   │   └── usecases/
│   │       ├── post/
│   │       │   ├── CreatePost.js
│   │       │   ├── GetPosts.js
│   │       │   ├── UpdatePost.js
│   │       │   └── DeletePost.js
│   │       ├── comment/
│   │       │   ├── CreateComment.js
│   │       │   └── GetComments.js
│   │       └── auth/
│   │           ├── RegisterUser.js
│   │           └── LoginUser.js
│   │
│   ├── adapters/                # Interface Layer
│   │   ├── controllers/
│   │   │   ├── PostController.js
│   │   │   ├── CommentController.js
│   │   │   └── AuthController.js
│   │   ├── repositories/
│   │   │   ├── PostRepositoryImpl.js
│   │   │   ├── CommentRepositoryImpl.js
│   │   │   └── UserRepositoryImpl.js
│   │   └── presenters/
│   │       └── JsonPresenter.js
│   │
│   ├── infrastructure/          # ชั้นนอกสุด - Framework & Tools
│   │   ├── database/
│   │   │   └── sqlite.js
│   │   ├── web/
│   │   │   ├── express.js
│   │   │   └── routes/
│   │   └── security/
│   │       └── jwt.js
│   │
│   └── interfaces/              # Contracts/Interfaces
│       └── repositories/
│           ├── IPostRepository.js
│           ├── ICommentRepository.js
│           └── IUserRepository.js
│
├── package.json
└── server.js
```

---

## 🎯 Step 1: Core Entities (ชั้นในสุด)

> 🎯 **Entities คำอธิบาย:**
>
> - Class ที่แทนข้อมูล business objects
> - ไม่มี dependency กับ database หรือ framework
> - เก็บเฉพาะ properties ที่เกี่ยวกับ business domain
> - มี validation methods เพื่อตรวจสอบความถูกต้องของข้อมูล
> - ตัวอย่าง: User ต้องมี email ที่ถูกต้อง และ password อย่างน้อย 6 ตัวอักษร

### `src/core/entities/User.js`

```javascript
class User {
  constructor({ id, username, email, password, createdAt }) {
    this.id = id;
    this.username = username;
    this.email = email;
    this.password = password;
    this.createdAt = createdAt;
  }

  // Business rules
  validateEmail() {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(this.email);
  }

  validatePassword() {
    return this.password && this.password.length >= 6;
  }

  validateUsername() {
    return this.username && this.username.length >= 3;
  }

  isValid() {
    return (
      this.validateEmail() && this.validatePassword() && this.validateUsername()
    );
  }

  toJSON() {
    return {
      id: this.id,
      username: this.username,
      email: this.email,
      createdAt: this.createdAt,
    };
  }
}

module.exports = User;
```

### `src/core/entities/Post.js`

```javascript
class Post {
  constructor({
    id,
    title,
    content,
    authorId,
    authorName,
    viewCount,
    createdAt,
    updatedAt,
  }) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.authorId = authorId;
    this.authorName = authorName;
    this.viewCount = viewCount || 0;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  // Business rules
  validateTitle() {
    return (
      this.title && this.title.trim().length >= 3 && this.title.length <= 200
    );
  }

  validateContent() {
    return this.content && this.content.trim().length >= 10;
  }

  isValid() {
    return this.validateTitle() && this.validateContent();
  }

  incrementViews() {
    this.viewCount += 1;
  }

  isOwnedBy(userId) {
    return this.authorId === userId;
  }

  toJSON() {
    return {
      id: this.id,
      title: this.title,
      content: this.content,
      authorId: this.authorId,
      authorName: this.authorName,
      viewCount: this.viewCount,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

module.exports = Post;
```

### `src/core/entities/Comment.js`

```javascript
class Comment {
  constructor({ id, postId, userId, username, content, createdAt }) {
    this.id = id;
    this.postId = postId;
    this.userId = userId;
    this.username = username;
    this.content = content;
    this.createdAt = createdAt;
  }

  // Business rules
  validateContent() {
    return (
      this.content &&
      this.content.trim().length >= 1 &&
      this.content.length <= 500
    );
  }

  isValid() {
    return this.validateContent();
  }

  isOwnedBy(userId) {
    return this.userId === userId;
  }

  toJSON() {
    return {
      id: this.id,
      postId: this.postId,
      userId: this.userId,
      username: this.username,
      content: this.content,
      createdAt: this.createdAt,
    };
  }
}

module.exports = Comment;
```

---

## 🔌 Step 2: Repository Interfaces

> 📋 **Interfaces คำอธิบาย:**
>
> - ทำให้ Core layer ไม่ขึ้นต่อ implementation เฉพาะเจาะจง
> - เป็นสัญญา (contract) ที่บอกว่า repository ต้องมี methods อะไรบ้าง
> - ในอนาคต สามารถเปลี่ยน database จาก SQLite เป็น MongoDB ได้ง่าย เพียงแต่เปลี่ยน implementation
> - Dependency Inversion Principle: Depend on abstractions, not on concretions

### `src/interfaces/repositories/IPostRepository.js`

```javascript
class IPostRepository {
  async create(post) {
    throw new Error("Method not implemented");
  }

  async findById(id) {
    throw new Error("Method not implemented");
  }

  async findAll() {
    throw new Error("Method not implemented");
  }

  async update(id, post) {
    throw new Error("Method not implemented");
  }

  async delete(id) {
    throw new Error("Method not implemented");
  }

  async incrementViewCount(id) {
    throw new Error("Method not implemented");
  }
}

module.exports = IPostRepository;
```

### `src/interfaces/repositories/IUserRepository.js`

```javascript
class IUserRepository {
  async create(user) {
    throw new Error("Method not implemented");
  }

  async findByEmail(email) {
    throw new Error("Method not implemented");
  }

  async findById(id) {
    throw new Error("Method not implemented");
  }
}

module.exports = IUserRepository;
```

### `src/interfaces/repositories/ICommentRepository.js`

```javascript
class ICommentRepository {
  async create(comment) {
    throw new Error("Method not implemented");
  }

  async findByPostId(postId) {
    throw new Error("Method not implemented");
  }

  async delete(id, userId) {
    throw new Error("Method not implemented");
  }
}

module.exports = ICommentRepository;
```

---

## 💼 Step 3: Use Cases

> 🎯 **Use Cases คำอธิบาย:**
>
> - ตัวแทนของ business logic แต่ละอย่าง
> - "Use Case" = สิ่งที่ user ต้องการทำ เช่น "สร้างบทความ", "ลบความเห็น"
> - ประกอบด้วย logic สำหรับ:
>   1. Validate ข้อมูล (ตรวจสอบความถูกต้อง)
>   2. Check permissions (อนุญาติให้ทำหรือไม่)
>   3. Call repository เพื่อบันทึก/อ่านข้อมูล
>   4. ส่ง events ถ้ามีการเปลี่ยนแปลง
> - ประโยชน์: Business logic แยกออกจาก HTTP/Database/Redis

### `src/core/usecases/post/CreatePost.js`

```javascript
const Post = require("../../entities/Post");

class CreatePost {
  constructor(postRepository) {
    this.postRepository = postRepository;
  }

  async execute({ title, content, authorId }) {
    // Create entity
    const post = new Post({
      title,
      content,
      authorId,
    });

    // Validate
    if (!post.isValid()) {
      throw new Error("Invalid post data");
    }

    // Save through repository
    const createdPost = await this.postRepository.create(post);

    return createdPost;
  }
}

module.exports = CreatePost;
```

### `src/core/usecases/post/GetPosts.js`

```javascript
class GetPosts {
  constructor(postRepository) {
    this.postRepository = postRepository;
  }

  async execute() {
    const posts = await this.postRepository.findAll();
    return posts;
  }
}

module.exports = GetPosts;
```

### `src/core/usecases/post/GetPostById.js`

```javascript
class GetPostById {
  constructor(postRepository) {
    this.postRepository = postRepository;
  }

  async execute(postId) {
    const post = await this.postRepository.findById(postId);

    if (!post) {
      throw new Error("Post not found");
    }

    // Increment view count
    await this.postRepository.incrementViewCount(postId);

    return post;
  }
}

module.exports = GetPostById;
```

### `src/core/usecases/post/UpdatePost.js`

```javascript
const Post = require("../../entities/Post");

class UpdatePost {
  constructor(postRepository) {
    this.postRepository = postRepository;
  }

  async execute({ postId, title, content, userId }) {
    // Find existing post
    const existingPost = await this.postRepository.findById(postId);

    if (!existingPost) {
      throw new Error("Post not found");
    }

    // Check ownership
    if (!existingPost.isOwnedBy(userId)) {
      throw new Error("Not authorized to update this post");
    }

    // Create updated post entity
    const updatedPost = new Post({
      ...existingPost,
      title,
      content,
    });

    // Validate
    if (!updatedPost.isValid()) {
      throw new Error("Invalid post data");
    }

    // Update
    return await this.postRepository.update(postId, updatedPost);
  }
}

module.exports = UpdatePost;
```

### `src/core/usecases/post/DeletePost.js`

```javascript
class DeletePost {
  constructor(postRepository) {
    this.postRepository = postRepository;
  }

  async execute({ postId, userId }) {
    // Find post
    const post = await this.postRepository.findById(postId);

    if (!post) {
      throw new Error("Post not found");
    }

    // Check ownership
    if (!post.isOwnedBy(userId)) {
      throw new Error("Not authorized to delete this post");
    }

    // Delete
    await this.postRepository.delete(postId);

    return { success: true };
  }
}

module.exports = DeletePost;
```

### `src/core/usecases/auth/RegisterUser.js`

```javascript
const User = require("../../entities/User");
const bcrypt = require("bcryptjs");

class RegisterUser {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async execute({ username, email, password }) {
    // Check if user exists
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new Error("Email already registered");
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create user entity
    const user = new User({
      username,
      email,
      password: hashedPassword,
    });

    // Validate
    if (!user.isValid()) {
      throw new Error("Invalid user data");
    }

    // Save
    const createdUser = await this.userRepository.create(user);

    return createdUser;
  }
}

module.exports = RegisterUser;
```

### `src/core/usecases/auth/LoginUser.js`

```javascript
const bcrypt = require("bcryptjs");

class LoginUser {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async execute({ email, password }) {
    // Find user
    const user = await this.userRepository.findByEmail(email);

    if (!user) {
      throw new Error("Invalid credentials");
    }

    // Verify password
    const isValid = await bcrypt.compare(password, user.password);

    if (!isValid) {
      throw new Error("Invalid credentials");
    }

    return user;
  }
}

module.exports = LoginUser;
```

### `src/core/usecases/comment/CreateComment.js`

```javascript
const Comment = require("../../entities/Comment");

class CreateComment {
  constructor(commentRepository, postRepository) {
    this.commentRepository = commentRepository;
    this.postRepository = postRepository;
  }

  async execute({ postId, userId, content }) {
    // Check if post exists
    const post = await this.postRepository.findById(postId);
    if (!post) {
      throw new Error("Post not found");
    }

    // Create comment entity
    const comment = new Comment({
      postId,
      userId,
      content,
    });

    // Validate
    if (!comment.isValid()) {
      throw new Error("Invalid comment data");
    }

    // Save
    const createdComment = await this.commentRepository.create(comment);

    return createdComment;
  }
}

module.exports = CreateComment;
```

---

## 🔧 Step 4: Repository Implementations

> 🗄️ **Repository Implementations คำอธิบาย:**
>
> - นี่คือ concrete implementation ของ Repository Interfaces
> - ส่วนที่เชื่อมต่อกับ database (SQLite)
> - รับ business objects (Entities) และบันทึกลงฐานข้อมูล
> - ดึงข้อมูลจาก database และแปลงเป็น Entities
> - ข้อดี: Use Cases ไม่รู้ว่าใช้ SQLite หรือ MongoDB ผ่านมา

### `src/adapters/repositories/PostRepositoryImpl.js`

```javascript
const IPostRepository = require("../../interfaces/repositories/IPostRepository");
const Post = require("../../core/entities/Post");

class PostRepositoryImpl extends IPostRepository {
  constructor(db) {
    super();
    this.db = db;
  }

  async create(post) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)",
        [post.title, post.content, post.authorId],
        function (err) {
          if (err) reject(err);
          else {
            resolve(
              new Post({
                id: this.lastID,
                title: post.title,
                content: post.content,
                authorId: post.authorId,
              }),
            );
          }
        },
      );
    });
  }

  async findById(id) {
    return new Promise((resolve, reject) => {
      this.db.get(
        `SELECT posts.*, users.username as author_name 
         FROM posts 
         JOIN users ON posts.author_id = users.id 
         WHERE posts.id = ?`,
        [id],
        (err, row) => {
          if (err) reject(err);
          else if (!row) resolve(null);
          else {
            resolve(
              new Post({
                id: row.id,
                title: row.title,
                content: row.content,
                authorId: row.author_id,
                authorName: row.author_name,
                viewCount: row.view_count,
                createdAt: row.created_at,
                updatedAt: row.updated_at,
              }),
            );
          }
        },
      );
    });
  }

  async findAll() {
    return new Promise((resolve, reject) => {
      this.db.all(
        `SELECT posts.*, users.username as author_name 
         FROM posts 
         JOIN users ON posts.author_id = users.id 
         ORDER BY posts.created_at DESC`,
        [],
        (err, rows) => {
          if (err) reject(err);
          else {
            const posts = rows.map(
              (row) =>
                new Post({
                  id: row.id,
                  title: row.title,
                  content: row.content,
                  authorId: row.author_id,
                  authorName: row.author_name,
                  viewCount: row.view_count,
                  createdAt: row.created_at,
                  updatedAt: row.updated_at,
                }),
            );
            resolve(posts);
          }
        },
      );
    });
  }

  async update(id, post) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "UPDATE posts SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
        [post.title, post.content, id],
        async (err) => {
          if (err) reject(err);
          else {
            const updated = await this.findById(id);
            resolve(updated);
          }
        },
      );
    });
  }

  async delete(id) {
    return new Promise((resolve, reject) => {
      this.db.run("DELETE FROM posts WHERE id = ?", [id], (err) => {
        if (err) reject(err);
        else resolve(true);
      });
    });
  }

  async incrementViewCount(id) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "UPDATE posts SET view_count = view_count + 1 WHERE id = ?",
        [id],
        (err) => {
          if (err) reject(err);
          else resolve(true);
        },
      );
    });
  }
}

module.exports = PostRepositoryImpl;
```

### `src/adapters/repositories/UserRepositoryImpl.js`

```javascript
const IUserRepository = require("../../interfaces/repositories/IUserRepository");
const User = require("../../core/entities/User");

class UserRepositoryImpl extends IUserRepository {
  constructor(db) {
    super();
    this.db = db;
  }

  async create(user) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "INSERT INTO users (username, email, password) VALUES (?, ?, ?)",
        [user.username, user.email, user.password],
        function (err) {
          if (err) reject(err);
          else {
            resolve(
              new User({
                id: this.lastID,
                username: user.username,
                email: user.email,
              }),
            );
          }
        },
      );
    });
  }

  async findByEmail(email) {
    return new Promise((resolve, reject) => {
      this.db.get(
        "SELECT * FROM users WHERE email = ?",
        [email],
        (err, row) => {
          if (err) reject(err);
          else if (!row) resolve(null);
          else {
            resolve(
              new User({
                id: row.id,
                username: row.username,
                email: row.email,
                password: row.password,
                createdAt: row.created_at,
              }),
            );
          }
        },
      );
    });
  }

  async findById(id) {
    return new Promise((resolve, reject) => {
      this.db.get("SELECT * FROM users WHERE id = ?", [id], (err, row) => {
        if (err) reject(err);
        else if (!row) resolve(null);
        else {
          resolve(
            new User({
              id: row.id,
              username: row.username,
              email: row.email,
              createdAt: row.created_at,
            }),
          );
        }
      });
    });
  }
}

module.exports = UserRepositoryImpl;
```

### `src/adapters/repositories/CommentRepositoryImpl.js`

```javascript
const ICommentRepository = require("../../interfaces/repositories/ICommentRepository");
const Comment = require("../../core/entities/Comment");

class CommentRepositoryImpl extends ICommentRepository {
  constructor(db) {
    super();
    this.db = db;
  }

  async create(comment) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)",
        [comment.postId, comment.userId, comment.content],
        function (err) {
          if (err) reject(err);
          else {
            resolve(
              new Comment({
                id: this.lastID,
                postId: comment.postId,
                userId: comment.userId,
                content: comment.content,
              }),
            );
          }
        },
      );
    });
  }

  async findByPostId(postId) {
    return new Promise((resolve, reject) => {
      this.db.all(
        `SELECT comments.*, users.username 
         FROM comments 
         JOIN users ON comments.user_id = users.id 
         WHERE comments.post_id = ? 
         ORDER BY comments.created_at DESC`,
        [postId],
        (err, rows) => {
          if (err) reject(err);
          else {
            const comments = rows.map(
              (row) =>
                new Comment({
                  id: row.id,
                  postId: row.post_id,
                  userId: row.user_id,
                  username: row.username,
                  content: row.content,
                  createdAt: row.created_at,
                }),
            );
            resolve(comments);
          }
        },
      );
    });
  }

  async delete(id, userId) {
    return new Promise((resolve, reject) => {
      this.db.run(
        "DELETE FROM comments WHERE id = ? AND user_id = ?",
        [id, userId],
        function (err) {
          if (err) reject(err);
          else resolve(this.changes > 0);
        },
      );
    });
  }
}

module.exports = CommentRepositoryImpl;
```

---

## 🎮 Step 5: Controllers (Adapters)

> 🎛️ **Controllers in Clean Architecture คำอธิบาย:**
>
> - ต่างจาก Phase 1 ที่ controller มี business logic เต็มไปหมด
> - ใน Phase 2 controller เป็นแค่ adapter ที่:
>   1. รับ HTTP request
>   2. แปลงเป็น use case input
>   3. เรียก use case
>   4. แปลง use case result เป็น HTTP response
> - Business logic อยู่ใน use cases ไม่ใช่ controller
> - ข้อดี: สามารถเปลี่ยน Express เป็น Fastify ได้โดยเปลี่ยนแค่ controller

### `src/adapters/controllers/PostController.js`

```javascript
class PostController {
  constructor({ createPost, getPosts, getPostById, updatePost, deletePost }) {
    this.createPost = createPost;
    this.getPosts = getPosts;
    this.getPostById = getPostById;
    this.updatePost = updatePost;
    this.deletePost = deletePost;
  }

  async handleGetAll(req, res) {
    try {
      const posts = await this.getPosts.execute();
      res.json({ posts: posts.map((p) => p.toJSON()) });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async handleGetOne(req, res) {
    try {
      const post = await this.getPostById.execute(req.params.id);
      res.json({ post: post.toJSON() });
    } catch (error) {
      if (error.message === "Post not found") {
        res.status(404).json({ error: error.message });
      } else {
        res.status(500).json({ error: error.message });
      }
    }
  }

  async handleCreate(req, res) {
    try {
      const { title, content } = req.body;
      const post = await this.createPost.execute({
        title,
        content,
        authorId: req.user.id,
      });
      res.status(201).json({ post: post.toJSON() });
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }

  async handleUpdate(req, res) {
    try {
      const { title, content } = req.body;
      const post = await this.updatePost.execute({
        postId: req.params.id,
        title,
        content,
        userId: req.user.id,
      });
      res.json({ post: post.toJSON() });
    } catch (error) {
      if (error.message.includes("Not authorized")) {
        res.status(403).json({ error: error.message });
      } else if (error.message === "Post not found") {
        res.status(404).json({ error: error.message });
      } else {
        res.status(400).json({ error: error.message });
      }
    }
  }

  async handleDelete(req, res) {
    try {
      await this.deletePost.execute({
        postId: req.params.id,
        userId: req.user.id,
      });
      res.json({ message: "Post deleted successfully" });
    } catch (error) {
      if (error.message.includes("Not authorized")) {
        res.status(403).json({ error: error.message });
      } else if (error.message === "Post not found") {
        res.status(404).json({ error: error.message });
      } else {
        res.status(500).json({ error: error.message });
      }
    }
  }
}

module.exports = PostController;
```

### `src/adapters/controllers/AuthController.js`

```javascript
const jwt = require("jsonwebtoken");

class AuthController {
  constructor({ registerUser, loginUser }) {
    this.registerUser = registerUser;
    this.loginUser = loginUser;
  }

  async handleRegister(req, res) {
    try {
      const { username, email, password } = req.body;

      const user = await this.registerUser.execute({
        username,
        email,
        password,
      });

      const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
        expiresIn: "7d",
      });

      res.status(201).json({
        message: "User registered successfully",
        user: user.toJSON(),
        token,
      });
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }

  async handleLogin(req, res) {
    try {
      const { email, password } = req.body;

      const user = await this.loginUser.execute({ email, password });

      const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
        expiresIn: "7d",
      });

      res.json({
        message: "Login successful",
        user: user.toJSON(),
        token,
      });
    } catch (error) {
      res.status(401).json({ error: error.message });
    }
  }
}

module.exports = AuthController;
```

### `src/adapters/controllers/CommentController.js`

```javascript
class CommentController {
  constructor({ createComment, getComments, deleteComment }) {
    this.createComment = createComment;
    this.getComments = getComments;
    this.deleteComment = deleteComment;
  }

  async handleGetComments(req, res) {
    try {
      const comments = await this.getComments.execute(req.params.postId);
      res.json({ comments: comments.map((c) => c.toJSON()) });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async handleCreate(req, res) {
    try {
      const { content } = req.body;
      const comment = await this.createComment.execute({
        postId: req.params.postId,
        userId: req.user.id,
        content,
      });
      res.status(201).json({ comment: comment.toJSON() });
    } catch (error) {
      if (error.message === "Post not found") {
        res.status(404).json({ error: error.message });
      } else {
        res.status(400).json({ error: error.message });
      }
    }
  }

  async handleDelete(req, res) {
    try {
      const deleted = await this.deleteComment.execute(
        req.params.id,
        req.user.id,
      );
      if (!deleted) {
        return res
          .status(404)
          .json({ error: "Comment not found or not authorized" });
      }
      res.json({ message: "Comment deleted successfully" });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

module.exports = CommentController;
```

---

## 🏗️ Step 6: Infrastructure Layer

> 🔧 **Infrastructure Layer คำอธิบาย:**
>
> - ชั้นนอกสุดที่มี tools อย่าง database, web framework, libraries
> - Database setup: SQLite connection และ tables creation
> - Middleware: Authentication logic ที่จัดการ JWT tokens
> - Express setup: Routes configuration และ middlewares
> - เปลี่ยน infrastructure ได้ง่ายโดยไม่ต้องแตะ business logic

### `src/infrastructure/database/sqlite.js`

```javascript
const sqlite3 = require("sqlite3").verbose();

function createDatabase() {
  const db = new sqlite3.Database(process.env.DB_PATH || "./database.sqlite");

  db.serialize(() => {
    db.run(`
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE NOT NULL,
        email TEXT UNIQUE NOT NULL,
        password TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `);

    db.run(`
      CREATE TABLE IF NOT EXISTS posts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        content TEXT NOT NULL,
        author_id INTEGER NOT NULL,
        view_count INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (author_id) REFERENCES users(id)
      )
    `);

    db.run(`
      CREATE TABLE IF NOT EXISTS comments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        post_id INTEGER NOT NULL,
        user_id INTEGER NOT NULL,
        content TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (post_id) REFERENCES posts(id),
        FOREIGN KEY (user_id) REFERENCES users(id)
      )
    `);
  });

  return db;
}

module.exports = { createDatabase };
```

### `src/infrastructure/web/middleware/authMiddleware.js`

```javascript
const jwt = require("jsonwebtoken");

class AuthMiddleware {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async authenticate(req, res, next) {
    try {
      const token = req.headers.authorization?.split(" ")[1];

      if (!token) {
        return res.status(401).json({ error: "Authentication required" });
      }

      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const user = await this.userRepository.findById(decoded.id);

      if (!user) {
        return res.status(401).json({ error: "User not found" });
      }

      req.user = user;
      next();
    } catch (error) {
      res.status(401).json({ error: "Invalid token" });
    }
  }
}

module.exports = AuthMiddleware;
```

### `src/infrastructure/web/express.js`

```javascript
const express = require("express");

function createExpressApp({
  postController,
  authController,
  commentController,
  authMiddleware,
}) {
  const app = express();

  app.use(express.json());

  // Health check
  app.get("/health", (req, res) => {
    res.json({
      status: "ok",
      message: "Blog API Phase 2 - Clean Architecture",
    });
  });

  // Auth routes
  app.post("/api/auth/register", (req, res) =>
    authController.handleRegister(req, res),
  );
  app.post("/api/auth/login", (req, res) =>
    authController.handleLogin(req, res),
  );

  // Post routes
  app.get("/api/posts", (req, res) => postController.handleGetAll(req, res));
  app.get("/api/posts/:id", (req, res) =>
    postController.handleGetOne(req, res),
  );
  app.post(
    "/api/posts",
    (req, res, next) => authMiddleware.authenticate(req, res, next),
    (req, res) => postController.handleCreate(req, res),
  );
  app.put(
    "/api/posts/:id",
    (req, res, next) => authMiddleware.authenticate(req, res, next),
    (req, res) => postController.handleUpdate(req, res),
  );
  app.delete(
    "/api/posts/:id",
    (req, res, next) => authMiddleware.authenticate(req, res, next),
    (req, res) => postController.handleDelete(req, res),
  );

  // Comment routes
  app.get("/api/comments/post/:postId", (req, res) =>
    commentController.handleGetComments(req, res),
  );
  app.post(
    "/api/comments/post/:postId",
    (req, res, next) => authMiddleware.authenticate(req, res, next),
    (req, res) => commentController.handleCreate(req, res),
  );
  app.delete(
    "/api/comments/:id",
    (req, res, next) => authMiddleware.authenticate(req, res, next),
    (req, res) => commentController.handleDelete(req, res),
  );

  return app;
}

module.exports = { createExpressApp };
```

---

## 🔗 Step 7: Dependency Injection (Main)

> 💉 **Dependency Injection คำอธิบาย:**
>
> - Inversion of Control (IoC): แทนที่จะให้ class สร้าง dependencies ของตัวเอง
> - เรา inject (ส่ง) dependencies เข้าไป
> - ข้อดี:
>   1. ง่ายต่อการ test (inject mock objects)
>   2. Loose coupling
>   3. สามารถเปลี่ยน implementation ได้ง่าย
> - ตัวอย่าง: PostController รับ createPost use case แทนที่จะสร้างเอง
> - ใน server.js เรา setup ทั้งหมด: repositories → use cases → controllers

### `server.js`

```javascript
require("dotenv").config();
const { createDatabase } = require("./src/infrastructure/database/sqlite");
const { createExpressApp } = require("./src/infrastructure/web/express");

// Repositories
const PostRepositoryImpl = require("./src/adapters/repositories/PostRepositoryImpl");
const UserRepositoryImpl = require("./src/adapters/repositories/UserRepositoryImpl");
const CommentRepositoryImpl = require("./src/adapters/repositories/CommentRepositoryImpl");

// Use Cases - Post
const CreatePost = require("./src/core/usecases/post/CreatePost");
const GetPosts = require("./src/core/usecases/post/GetPosts");
const GetPostById = require("./src/core/usecases/post/GetPostById");
const UpdatePost = require("./src/core/usecases/post/UpdatePost");
const DeletePost = require("./src/core/usecases/post/DeletePost");

// Use Cases - Auth
const RegisterUser = require("./src/core/usecases/auth/RegisterUser");
const LoginUser = require("./src/core/usecases/auth/LoginUser");

// Use Cases - Comment
const CreateComment = require("./src/core/usecases/comment/CreateComment");
const GetComments = require("./src/core/usecases/comment/GetComments");
const DeleteComment = require("./src/core/usecases/comment/DeleteComment");

// Controllers
const PostController = require("./src/adapters/controllers/PostController");
const AuthController = require("./src/adapters/controllers/AuthController");
const CommentController = require("./src/adapters/controllers/CommentController");

// Middleware
const AuthMiddleware = require("./src/infrastructure/web/middleware/authMiddleware");

// Initialize database
const db = createDatabase();

// Initialize repositories
const postRepository = new PostRepositoryImpl(db);
const userRepository = new UserRepositoryImpl(db);
const commentRepository = new CommentRepositoryImpl(db);

// Initialize use cases - Post
const createPost = new CreatePost(postRepository);
const getPosts = new GetPosts(postRepository);
const getPostById = new GetPostById(postRepository);
const updatePost = new UpdatePost(postRepository);
const deletePost = new DeletePost(postRepository);

// Initialize use cases - Auth
const registerUser = new RegisterUser(userRepository);
const loginUser = new LoginUser(userRepository);

// Initialize use cases - Comment
const createComment = new CreateComment(commentRepository, postRepository);
const getComments = new GetComments(commentRepository);
const deleteComment = new DeleteComment(commentRepository);

// Initialize controllers
const postController = new PostController({
  createPost,
  getPosts,
  getPostById,
  updatePost,
  deletePost,
});

const authController = new AuthController({
  registerUser,
  loginUser,
});

const commentController = new CommentController({
  createComment,
  getComments,
  deleteComment,
});

// Initialize middleware
const authMiddleware = new AuthMiddleware(userRepository);

// Create Express app
const app = createExpressApp({
  postController,
  authController,
  commentController,
  authMiddleware,
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
  console.log(`📝 Phase 2: Clean Architecture`);
  console.log(`✅ Dependency Injection: Active`);
});
```

### เพิ่ม Use Case สำหรับ Comment

### `src/core/usecases/comment/GetComments.js`

```javascript
class GetComments {
  constructor(commentRepository) {
    this.commentRepository = commentRepository;
  }

  async execute(postId) {
    const comments = await this.commentRepository.findByPostId(postId);
    return comments;
  }
}

module.exports = GetComments;
```

### `src/core/usecases/comment/DeleteComment.js`

```javascript
class DeleteComment {
  constructor(commentRepository) {
    this.commentRepository = commentRepository;
  }

  async execute(commentId, userId) {
    const deleted = await this.commentRepository.delete(commentId, userId);
    return deleted;
  }
}

module.exports = DeleteComment;
```

---

## ✅ ทดสอบ Phase 2

```bash
npm run dev
```

API endpoints เหมือนเดิม แต่ architecture เปลี่ยนไป!

---

## 📊 สรุป Phase 2

✅ **ข้อดีที่ได้:**

- Business logic แยกออกจาก framework
- ง่ายต่อการ test (mock repositories ได้)
- Dependency Inversion - core ไม่ depend infrastructure
- เปลี่ยน database ได้ง่าย (แค่เปลี่ยน repository)
- เปลี่ยน framework ได้ง่าย (แค่เปลี่ยน adapter layer)

> 📝 **สรุปข้อสำคัญ Phase 2:**
>
> - ทำให้โค้ดเป็นระบบ สะดวกต่อการพัฒนาและบำรุงรักษา
> - Entities และ Use Cases ไม่ต้องรู้เรื่อง framework หรือ database
> - สามารถเขียน unit tests โดยไม่ต้อง mock database
> - เปลี่ยน technology stack ได้ง่ายในอนาคต

🎯 **Key Principles:**

- **Entities:** Pure business objects
- **Use Cases:** Application business rules
- **Repositories:** Data access abstraction
- **Controllers:** Adapt use cases to web
- **Dependency Flow:** เข้าหาชั้นใน (ไม่ออกนอก)

---

# Phase 3: Add Event-Driven with Redis

> ⚡ **Phase 3 คำอธิบาย:**
>
> - Event-Driven Architecture เป็นรูปแบบที่มี "events" เป็นศูนย์กลาง
> - Events คือ "สิ่งที่เกิดขึ้น" เช่น "POST_CREATED" "COMMENT_ADDED"
> - Pub/Sub pattern: บาง components publish events บาง components subscribe (ฟังรอ) events
> - Redis Pub/Sub: ใช้ Redis สำหรับ messaging ระหว่าง services
> - Loose Coupling: Components ไม่ต้องรู้จักกันโดยตรง พวกเขาติดต่อผ่าน events
> - Async Processing: Event handlers ทำงาน asynchronously (ไม่ต้องรอ)
> - Scalability: สามารถเพิ่มอีก services ที่ฟังรอ events ได้ง่าย

## 🎯 เป้าหมาย

เพิ่ม Event system เพื่อ:

- แจ้งเตือนเมื่อมี Post ใหม่
- แจ้งเตือนเมื่อมี Comment ใหม่
- อัพเดท Analytics
- ส่ง Notifications (จำลอง)

---

## 📁 โครงสร้างเพิ่มเติม

```
blog-phase3/
├── src/
│   ├── core/
│   │   ├── events/              # ✨ ใหม่
│   │   │   ├── PostCreatedEvent.js
│   │   │   └── CommentCreatedEvent.js
│   │   └── ...
│   ├── infrastructure/
│   │   ├── events/              # ✨ ใหม่
│   │   │   ├── RedisEventBus.js
│   │   │   └── EventHandlers.js
│   │   └── ...
│   └── ...
└── ...
```

---

## 🚀 Step 1: Setup Redis

### ติดตั้ง Dependencies

```bash
npm install redis ioredis
```

> 📦 **Dependencies คำอธิบาย:**
>
> - redis: Official Redis client library
> - ioredis: Alternative Redis client ที่ supports Promises และ async/await (ใช้ในคู่มือนี้)

### รัน Redis ด้วย Docker

```bash
# รัน Redis
docker run -d --name redis -p 6379:6379 redis:alpine

# ตรวจสอบว่ารันอยู่
docker ps
```

> 🐳 **Docker คำอธิบาย:**
>
> - `-d`: รัน in background (detached)
> - `--name redis`: ตั้งชื่อ container
> - `-p 6379:6379`: expose port 6379 (default Redis port)
> - `redis:alpine`: ใช้ lightweight Alpine Linux image

### หรือติดตั้ง Redis แบบ Local

**macOS:**

```bash
brew install redis
brew services start redis
```

**Ubuntu:**

```bash
sudo apt-get install redis-server
sudo systemctl start redis
```

**Windows:**
Download จาก https://redis.io/download

---

## 📦 Step 2: Event Entities

> 📋 **Event Entities คำอธิบาย:**
>
> - Class ที่แทนขข้อมูลของ events
> - ทุก event มี: eventType (ชื่อ), timestamp (เวลา), data (ข้อมูล)
> - เมื่อ event ถูก publish ไปยัง Redis มันจะถูก serialize เป็น JSON
> - Event handlers subscribe เพื่อฟังรอ events และทำการประมวลผล

### `src/core/events/PostCreatedEvent.js`

```javascript
class PostCreatedEvent {
  constructor(post) {
    this.eventType = "POST_CREATED";
    this.timestamp = new Date().toISOString();
    this.data = {
      postId: post.id,
      title: post.title,
      authorId: post.authorId,
      authorName: post.authorName,
      createdAt: post.createdAt,
    };
  }

  toJSON() {
    return {
      eventType: this.eventType,
      timestamp: this.timestamp,
      data: this.data,
    };
  }
}

module.exports = PostCreatedEvent;
```

### `src/core/events/CommentCreatedEvent.js`

```javascript
class CommentCreatedEvent {
  constructor(comment, post) {
    this.eventType = "COMMENT_CREATED";
    this.timestamp = new Date().toISOString();
    this.data = {
      commentId: comment.id,
      postId: comment.postId,
      postTitle: post?.title,
      userId: comment.userId,
      username: comment.username,
      content: comment.content,
      createdAt: comment.createdAt,
    };
  }

  toJSON() {
    return {
      eventType: this.eventType,
      timestamp: this.timestamp,
      data: this.data,
    };
  }
}

module.exports = CommentCreatedEvent;
```

---

## 🔌 Step 3: Event Bus Interface

### `src/interfaces/IEventBus.js`

```javascript
class IEventBus {
  async publish(event) {
    throw new Error("Method not implemented");
  }

  async subscribe(eventType, handler) {
    throw new Error("Method not implemented");
  }
}

module.exports = IEventBus;
```

> 📋 **Event Bus Interface คำอธิบาย:**
>
> - Interface (สัญญา) ที่บอกว่า Event Bus ต้องมี methods อะไร
> - publish(event): ส่ง event ไปยัง subscribers
> - subscribe(eventType, handler): ฟังรอ events ของบางประเภท
> - สามารถใช้ Redis pub/sub หรือ message queue อื่นๆ โดยเปลี่ยนแค่ implementation
> - Dependency Inversion: Use cases ไม่ต้องรู้ว่าใช้ Redis หรือ RabbitMQ

---

## ⚡ Step 4: Redis Event Bus Implementation

> ⚡ **Redis Event Bus คำอธิบาย:**
>
> - Redis Pub/Sub คือ messaging pattern ง่ายๆ
> - publisher: ส่ง messages ไปยัง channel
> - subscribers: ฟังรอและรับ messages จาก channels
> - ไม่เก็บ message history (ต่างจาก message queues)
> - Fast และ Lightweight ดีสำหรับ real-time updates
> - ioredis: Library ที่ทำให้ง่ายต่อการใช้ Redis

### `src/infrastructure/events/RedisEventBus.js`

```javascript
const Redis = require("ioredis");
const IEventBus = require("../../interfaces/IEventBus");

class RedisEventBus extends IEventBus {
  constructor() {
    super();
    this.publisher = new Redis({
      host: process.env.REDIS_HOST || "localhost",
      port: process.env.REDIS_PORT || 6379,
      retryStrategy: (times) => {
        return Math.min(times * 50, 2000);
      },
    });

    this.subscriber = new Redis({
      host: process.env.REDIS_HOST || "localhost",
      port: process.env.REDIS_PORT || 6379,
    });

    this.handlers = new Map();

    this.subscriber.on("message", (channel, message) => {
      this.handleMessage(channel, message);
    });

    console.log("✅ Redis Event Bus initialized");
  }

  async publish(event) {
    try {
      const channel = event.eventType;
      const message = JSON.stringify(event.toJSON());

      await this.publisher.publish(channel, message);

      console.log(`📤 Event published: ${event.eventType}`, event.data);
    } catch (error) {
      console.error("Error publishing event:", error);
    }
  }

  async subscribe(eventType, handler) {
    try {
      // Store handler
      if (!this.handlers.has(eventType)) {
        this.handlers.set(eventType, []);
        await this.subscriber.subscribe(eventType);
        console.log(`🎧 Subscribed to: ${eventType}`);
      }

      this.handlers.get(eventType).push(handler);
    } catch (error) {
      console.error("Error subscribing to event:", error);
    }
  }

  handleMessage(channel, message) {
    try {
      const event = JSON.parse(message);
      const handlers = this.handlers.get(channel) || [];

      handlers.forEach((handler) => {
        try {
          handler(event);
        } catch (error) {
          console.error(`Error in event handler for ${channel}:`, error);
        }
      });
    } catch (error) {
      console.error("Error handling message:", error);
    }
  }

  async disconnect() {
    await this.publisher.quit();
    await this.subscriber.quit();
  }
}

module.exports = RedisEventBus;
```

---

## 🎯 Step 5: Event Handlers

### `src/infrastructure/events/EventHandlers.js`

> 🎯 **Event Handlers คำอธิบาย:**
>
> - Functions ที่ตอบสนองต่อ events
> - เมื่อ POST_CREATED event ถูก publish มี 4 handlers ที่ทำงาน:
>   1. handlePostCreated: บันทึกลง analytics
>   2. handlePostCreatedNotification: ส่งแจ้งเตือนไปยัง followers
>   3. handleNewPostEmail: เพิ่ม email job เข้า queue
>   4. handlePostCreatedIndex: ส่งดัชนีไปยัง search service
> - ข้อดี: ไม่ต้องแก้ไข CreatePost use case เมื่อต้องการเพิ่ม handler ใหม่
> - Open/Closed Principle: Open for extension, closed for modification

```javascript
class EventHandlers {
  // Analytics Handler
  static handlePostCreated(event) {
    console.log("📊 Analytics: New post created", {
      postId: event.data.postId,
      title: event.data.title,
      author: event.data.authorName,
    });

    // In real app: Send to analytics service
    // analyticsService.track('post_created', event.data);
  }

  static handleCommentCreated(event) {
    console.log("📊 Analytics: New comment added", {
      commentId: event.data.commentId,
      postId: event.data.postId,
      username: event.data.username,
    });
  }

  // Notification Handler
  static handlePostCreatedNotification(event) {
    console.log("🔔 Notification: Send to followers", {
      postId: event.data.postId,
      title: event.data.title,
      author: event.data.authorName,
    });

    // In real app: Send push notification
    // notificationService.sendToFollowers(event.data.authorId, event.data);
  }

  static handleCommentCreatedNotification(event) {
    console.log("🔔 Notification: Alert post author", {
      postId: event.data.postId,
      postTitle: event.data.postTitle,
      commenter: event.data.username,
    });

    // In real app: Notify post author
    // notificationService.notifyAuthor(event.data.postId, event.data);
  }

  // Email Handler
  static handleNewPostEmail(event) {
    console.log("📧 Email: Send digest to subscribers", {
      postTitle: event.data.title,
    });

    // In real app: Queue email job
    // emailQueue.add('new-post-digest', event.data);
  }

  // Search Index Handler
  static handlePostCreatedIndex(event) {
    console.log("🔍 Search Index: Index new post", {
      postId: event.data.postId,
      title: event.data.title,
    });

    // In real app: Update Elasticsearch
    // searchService.indexPost(event.data);
  }
}

module.exports = EventHandlers;
```

---

## 🔄 Step 6: แก้ไข Use Cases ให้ Publish Events

### `src/core/usecases/post/CreatePost.js` (แก้ไข)

```javascript
const Post = require("../../entities/Post");
const PostCreatedEvent = require("../../events/PostCreatedEvent");

class CreatePost {
  constructor(postRepository, eventBus) {
    this.postRepository = postRepository;
    this.eventBus = eventBus;
  }

  async execute({ title, content, authorId }) {
    // Create entity
    const post = new Post({
      title,
      content,
      authorId,
    });

    // Validate
    if (!post.isValid()) {
      throw new Error("Invalid post data");
    }

    // Save
    const createdPost = await this.postRepository.create(post);

    // Publish event ✨
    if (this.eventBus) {
      const event = new PostCreatedEvent(createdPost);
      await this.eventBus.publish(event);
    }

    return createdPost;
  }
}

module.exports = CreatePost;
```

> 📋 **Publishing Events คำอธิบาย:**
>
> - Use Case ตอนจบ ให้ publish event เพื่อบอกระบบว่า "เกิดอะไรขึ้น"
> - Event bus ส่วน subscribers จะได้รับข้อมูลและทำการประมวลผล
> - Use Case ไม่ต้องรู้ว่า subscribers ทำอะไรต่อ event นี้
> - Separation of Concerns: Post creation logic แยกจาก side effects

### `src/core/usecases/comment/CreateComment.js` (แก้ไข)

```javascript
const Comment = require("../../entities/Comment");
const CommentCreatedEvent = require("../../events/CommentCreatedEvent");

class CreateComment {
  constructor(commentRepository, postRepository, eventBus) {
    this.commentRepository = commentRepository;
    this.postRepository = postRepository;
    this.eventBus = eventBus;
  }

  async execute({ postId, userId, content }) {
    // Check if post exists
    const post = await this.postRepository.findById(postId);
    if (!post) {
      throw new Error("Post not found");
    }

    // Create comment
    const comment = new Comment({
      postId,
      userId,
      content,
    });

    // Validate
    if (!comment.isValid()) {
      throw new Error("Invalid comment data");
    }

    // Save
    const createdComment = await this.commentRepository.create(comment);

    // Publish event ✨
    if (this.eventBus) {
      const event = new CommentCreatedEvent(createdComment, post);
      await this.eventBus.publish(event);
    }

    return createdComment;
  }
}

module.exports = CreateComment;
```

---

## 🔗 Step 7: แก้ไข Dependency Injection

### อัพเดท `.env`

```env
PORT=3000
JWT_SECRET=your_super_secret_key_change_this
DB_PATH=./database.sqlite

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
```

> 📝 **Configuration คำอธิบาย:**
>
> - Environment variables ควรอยู่ใน .env ไม่ควร hard-code
> - ทำให้เปลี่ยน server address และ port ได้ง่าย
> - Dev server กับ production server ใช้ config ต่างกัน

### อัพเดท `server.js`

```javascript
require("dotenv").config();
const { createDatabase } = require("./src/infrastructure/database/sqlite");
const { createExpressApp } = require("./src/infrastructure/web/express");
const RedisEventBus = require("./src/infrastructure/events/RedisEventBus");
const EventHandlers = require("./src/infrastructure/events/EventHandlers");

// ... (repositories เหมือนเดิม)

// Initialize Event Bus ✨
const eventBus = new RedisEventBus();

// Setup Event Handlers ✨
eventBus.subscribe("POST_CREATED", EventHandlers.handlePostCreated);
eventBus.subscribe("POST_CREATED", EventHandlers.handlePostCreatedNotification);
eventBus.subscribe("POST_CREATED", EventHandlers.handleNewPostEmail);
eventBus.subscribe("POST_CREATED", EventHandlers.handlePostCreatedIndex);

eventBus.subscribe("COMMENT_CREATED", EventHandlers.handleCommentCreated);
eventBus.subscribe(
  "COMMENT_CREATED",
  EventHandlers.handleCommentCreatedNotification,
);

// Initialize use cases with eventBus
const createPost = new CreatePost(postRepository, eventBus);
const createComment = new CreateComment(
  commentRepository,
  postRepository,
  eventBus,
);

// ... (ส่วนอื่นเหมือนเดิม)

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
  console.log(`📝 Phase 3: Event-Driven Architecture`);
  console.log(`⚡ Redis Event Bus: Active`);
});

// Graceful shutdown
process.on("SIGINT", async () => {
  console.log("\n🛑 Shutting down...");
  await eventBus.disconnect();
  process.exit(0);
});
```

> 🔧 **DI Setup คำอธิบาย:**
>
> - Setup ทั้ง dependency graph ในจุดเดียว
> - เมื่อต้องการ test สามารถ inject mock objects ได้ง่าย
> - Event handlers subscribe แบบ automatically
> - Graceful shutdown ปิด Redis connection อย่างถูกต้อง

---

## ✅ Step 8: ทดสอบ Event System

### รันโปรเจกต์

```bash
# Terminal 1: รัน Redis
docker run -d --name redis -p 6379:6379 redis:alpine

# Terminal 2: รัน Application
npm run dev
```

### ทดสอบสร้าง Post

```bash
# สร้าง post
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "Event-Driven Test",
    "content": "Testing event system!"
  }'
```

> 🧪 **ทดสอบ Event คำอธิบาย:**
>
> - เมื่อสร้าง Post ให้ดูที่ console ของ application
> - ควรเห็น "📤 Event published" แล้วตามด้วย 4 handlers ทำงาน
> - ทั้งหมดเกิดขึ้นใน milliseconds

### ผลลัพธ์ที่ควรเห็นใน Console

```
📤 Event published: POST_CREATED { postId: 1, title: 'Event-Driven Test', ... }
📊 Analytics: New post created { postId: 1, title: 'Event-Driven Test', ... }
🔔 Notification: Send to followers { postId: 1, ... }
📧 Email: Send digest to subscribers { postTitle: 'Event-Driven Test' }
🔍 Search Index: Index new post { postId: 1, ... }
```

---

## 🎨 Step 9: เพิ่ม Event Viewer (Optional)

### `src/infrastructure/events/EventViewer.js`

```javascript
const Redis = require("ioredis");

class EventViewer {
  constructor() {
    this.subscriber = new Redis();

    // Subscribe to all events
    this.subscriber.psubscribe("*");

    this.subscriber.on("pmessage", (pattern, channel, message) => {
      const event = JSON.parse(message);
      this.displayEvent(channel, event);
    });

    console.log("👀 Event Viewer started...\n");
  }

  displayEvent(channel, event) {
    console.log("=".repeat(60));
    console.log(`📡 Event: ${channel}`);
    console.log(`⏰ Time: ${event.timestamp}`);
    console.log(`📦 Data:`, JSON.stringify(event.data, null, 2));
    console.log("=".repeat(60));
    console.log("");
  }
}

// Run standalone
if (require.main === module) {
  new EventViewer();
}

module.exports = EventViewer;
```

> 👁️ **Event Viewer คำอธิบาย:**
>
> - Tool ที่ดูว่ามี events อะไรถูก publish
> - `psubscribe("*")` = subscribe รอ all events
> - เมื่อ event ถูก publish จะแสดง event details
> - ประโยชน์: debug และดู events flow ในระบบ

### รัน Event Viewer

```bash
# Terminal 3: Event Monitor
node src/infrastructure/events/EventViewer.js
```

---

## 📊 สรุป Phase 3

✅ **สิ่งที่ได้เพิ่ม:**

- Event-Driven Architecture ด้วย Redis
- Pub/Sub pattern สำหรับ loose coupling
- Multiple event handlers ต่อ 1 event
- Async processing capability
- Scalable event system

> 📝 **สรุปข้อสำคัญ Phase 3:**
>
> - ลบความเชื่อมโยง (coupling) ระหว่าง components
> - เพิ่ม handlers ใหม่โดยไม่ต้องแก้ไข existing code
> - สร้างระบบที่ scalable สำหรับแอปพลิเคชันขนาดใหญ่
> - Event Sourcing foundation สำหรับขั้นตอนต่อไป

🎯 **Use Cases ที่ทำได้:**

- Real-time analytics
- Notification system
- Email queuing
- Search indexing
- Audit logging
- Cache invalidation

---

## 📈 การเปรียบเทียบทั้ง 3 Phases

### Phase 1: Simple Express (MVC-like)

```
✅ รวดเร็วในการพัฒนา
✅ เหมาะกับโปรเจกต์เล็ก
❌ Tight coupling
❌ ยาก test
❌ ยากต่อการขยาย
```

> 📝 **ใช้เมื่อ:** โปรเจกต์เล็ก prototype proof of concept

### Phase 2: Clean Architecture

```
✅ Loose coupling
✅ ง่ายต่อการ test
✅ เปลี่ยน technology ได้ง่าย
✅ Business logic ชัดเจน
⚠️ ซับซ้อนกว่า
⚠️ Code เยอะขึ้น
```

> 📝 **ใช้เมื่อ:** โปรเจกต์ขนาดกลาง ที่ต้อง maintainability

### Phase 3: + Event-Driven

```
✅ ทุกอย่างใน Phase 2
✅ Async processing
✅ Scalability สูง
✅ Real-time capabilities
⚠️ ต้องจัดการ distributed system
⚠️ ต้องมี infrastructure เพิ่ม
```

> 📝 **ใช้เมื่อ:** โปรเจกต์ขนาดใหญ่ ต้อง real-time features scalability

---

## 🎓 สิ่งที่ได้เรียนรู้

### Architecture Patterns

- ✅ MVC Pattern
- ✅ Clean Architecture (Hexagonal)
- ✅ Event-Driven Architecture
- ✅ Repository Pattern
- ✅ Dependency Injection

> 🎓 **Architecture คำอธิบาย:**
>
> - MVC: ง่าย แต่ tight coupling
> - Clean: ง่ายต่อการ test แต่เปลี่ยน technology ง่าย
> - Event-Driven: Async loose coupling scalable

### Principles

- ✅ SOLID Principles
- ✅ Separation of Concerns
- ✅ Dependency Inversion
- ✅ Loose Coupling
- ✅ Single Responsibility

> 📝 **SOLID Principles:**
>
> - **S**ingle Responsibility: Class ต้องมี 1 เหตุผล
> - **O**pen/Closed: Open for extension closed for modification
> - **L**iskov Substitution: Subclass ต้อง substitute parent
> - **I**nterface Segregation: ใช้ specific interfaces
> - **D**ependency Inversion: Depend on abstractions

### Technologies

- ✅ Node.js + Express
- ✅ SQLite Database
- ✅ Redis Pub/Sub
- ✅ JWT Authentication
- ✅ Event Sourcing (basic)

---

## 🚀 Next Steps

### ขยายโปรเจกต์เพิ่มเติม

1. **Testing:**
   - Unit tests สำหรับ Use Cases
   - Integration tests สำหรับ Repositories
   - E2E tests สำหรับ API

2. **Features:**
   - User profiles
   - Post categories/tags
   - File uploads
   - Post reactions (like/dislike)

3. **Advanced Patterns:**
   - CQRS (Command Query Responsibility Segregation)
   - Event Sourcing
   - Saga Pattern
   - API Gateway

4. **DevOps:**
   - Docker Compose setup
   - CI/CD pipeline
   - Monitoring & Logging
   - Load testing

> 💡 **Next Steps คำอธิบาย:**
>
> - Testing: ต้องมี test cases สำหรับ production code
> - Features: โปรเจกต์จริงจะซับซ้อนมากขึ้น
> - Advanced Patterns: เมื่อระบบใหญ่ขึ้นต้องใช้ pattern เพิ่มเติม
> - DevOps: Production deployment ต้อง infrastructure ทั้งหมด

---

## 📚 Resources

### Documentation

- [Clean Architecture - Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/)
- [Event-Driven Architecture](https://martinfowler.com/articles/201701-event-driven.html)

### Books

- Clean Architecture - Robert C. Martin
- Domain-Driven Design - Eric Evans
- Microservices Patterns - Chris Richardson

> 📖 **Resources คำอธิบาย:**
>
> - Uncle Bob: หนึ่งใน software architecture ที่มีชื่อเสียง
> - Martin Fowler: Architectural patterns ที่ใช้ในอุตสาหกรรม
> - Domain-Driven Design: เจาะลึกการออกแบบ business logic

---

**🎉 ตอนนี้ คุณได้สร้าง Blog Application ผ่าน 3 Architecture Patterns แล้ว**

> 🌟 **สรุปทั้งหมด:**
>
> - Phase 1: รู้จัก MVC ธรรมชาติและข้อจำกัด
> - Phase 2: Clean Architecture ทำให้ code maintainable testable
> - Phase 3: Event-Driven ทำให้ system scalable asynchronous
> - เหล่านี้คือ foundation ของ modern web development
