📙 Phần 3 · Xây Dựng Web · Chương 8

Full-stack Projects

Kết hợp tất cả kiến thức Frontend và Backend để xây dựng 2 ứng dụng hoàn chỉnh: Todo App và Blog Platform — từ lập kế hoạch đến deploy.

8 giờ học
📝 2 dự án thực tế
🚀 Deploy lên cloud
📊 Mức độ: Nâng cao

🎯 Mục tiêu học tập

  • Lập kế hoạch dự án full-stack với AI từ đầu
  • Xây dựng Todo App: React + Express + SQLite với auth
  • Xây dựng Blog Platform: file upload + rich text editor
  • Deploy frontend lên Vercel, backend lên Railway
1

Bài 8.1 — Lập Kế Hoạch Dự Án với AI

User Story
Mô tả tính năng từ góc nhìn người dùng: "Là [người dùng], tôi muốn [làm gì] để [đạt mục tiêu]". Nền tảng để AI plan project.
API Contract
Thỏa thuận giữa frontend và backend: endpoint nào, method gì, request body format, response format. Viết trước khi code.
Monorepo
Cấu trúc lưu cả frontend và backend trong 1 repository. Ưu điểm: dễ share types, 1 PR thay đổi cả 2 phía, CI/CD đơn giản.
Environment Variable
Biến môi trường lưu config nhạy cảm (DB URL, API keys, secrets). File .env cho local, inject qua CI/CD system cho production.

Quy trình "Viết Spec" với AI

Trước khi code, dùng AI để lập kế hoạch kỹ càng. Đây là bước tiết kiệm thời gian nhất.

1

Mô tả ứng dụng bằng user stories

Viết danh sách: "Là người dùng, tôi muốn [làm gì] để [đạt mục tiêu]". Càng cụ thể càng tốt. Paste vào Copilot Chat để AI giúp break down thành tasks.

2

AI tạo Database Schema

Paste user stories, nhờ AI thiết kế Prisma schema. Review kỹ relationships, indexes, và constraints.

3

AI tạo API Contract

Nhờ AI liệt kê tất cả endpoints cần thiết (method, path, request body, response format). Đây là "contract" giữa frontend và backend.

4

Code từng feature nhỏ

Dùng AI để code theo từng endpoint. Mỗi feature = 1 prompt rõ ràng với context đầy đủ.

text — Prompt lập kế hoạch
Tôi muốn build một Blog Platform. Tôi đã có:
- Backend: Express.js + Prisma + SQLite (users, posts schema)
- Frontend: React + Vite + Tailwind CSS

User stories:
1. Tác giả đăng ký/đăng nhập
2. Tác giả tạo/sửa/xóa bài viết (có rich text editor)
3. Upload ảnh thumbnail cho bài viết
4. Người đọc xem danh sách bài viết (phân trang)
5. Người đọc xem chi tiết bài viết
6. Tìm kiếm bài viết theo tiêu đề
7. Lọc bài viết theo tag

Hãy giúp tôi:
1. Thiết kế API endpoints đầy đủ (REST)
2. Thứ tự implement từng feature hợp lý
3. Các package npm cần cài thêm

2

Bài 8.2 — Dự Án 1: Todo App Full-stack

Quy Trình Full-Stack Project từ Đầu

1
User Stories → API Contract

Paste user stories vào Copilot, nhờ AI thiết kế API endpoints. Output: danh sách đầy đủ method + path + body + response.

2
Database Schema

Từ API contract, AI thiết kế Prisma schema. Review relationships và indexes. Run npx prisma migrate dev.

3
Backend CRUD trước

Code và test toàn bộ API endpoints với curl/REST Client. Backend hoạt động đúng TRƯỚC KHI viết bất kỳ React nào.

4
Component Tree Frontend

List tất cả components cần có. Tách thành: Page components, Feature components, UI components. AI generate component structure.

5
Connect Frontend ↔ Backend

Tạo API layer (src/api/), custom hooks để fetch data. Xử lý loading, error states. Test với backend thật chạy local.

6
Deploy & CI/CD

Backend lên Railway/Render, Frontend lên Vercel. Setup GitHub Actions để auto-deploy khi push main. Add health check endpoint.

🏗️
Lợi ích của Monorepo

Lưu frontend + backend trong cùng 1 repo giúp: chia sẻ TypeScript types giữa 2 phía, 1 PR thay đổi đồng thời, CI/CD đơn giản hơn, và dễ onboard team member mới. Dùng npm workspaces hoặc turborepo cho monorepo chuyên nghiệp.

Cấu trúc project monorepo

text
todo-app/
├── backend/          ← Express API (Port 3000)
│   ├── src/
│   │   ├── routes/todos.js
│   │   ├── controllers/todoController.js
│   │   ├── middleware/auth.js
│   │   └── app.js
│   ├── prisma/schema.prisma
│   └── package.json
├── frontend/         ← React + Vite (Port 5173)
│   ├── src/
│   │   ├── api/todoApi.js
│   │   ├── components/
│   │   │   ├── TodoList.jsx
│   │   │   ├── TodoItem.jsx
│   │   │   └── AddTodoForm.jsx
│   │   ├── hooks/useTodos.js
│   │   └── App.jsx
│   └── package.json
└── README.md

Backend: Todo API

javascript — routes/todos.js
const router = require('express').Router();
const ctrl   = require('../controllers/todoController');
const { requireAuth } = require('../middleware/auth');

// Tất cả todo routes đều yêu cầu auth
router.use(requireAuth);

router.get('/',      ctrl.getTodos);   // GET  /api/todos
router.post('/',     ctrl.createTodo); // POST /api/todos
router.patch('/:id', ctrl.updateTodo); // PATCH /api/todos/:id
router.delete('/:id',ctrl.deleteTodo); // DELETE /api/todos/:id

module.exports = router;
javascript — controllers/todoController.js
const { PrismaClient } = require('@prisma/client');
const { z } = require('zod');
const prisma = new PrismaClient();

const todoSchema = z.object({
  title:    z.string().min(1).max(200),
  priority: z.enum(['low', 'medium', 'high']).optional().default('medium'),
  dueDate:  z.string().datetime().optional().nullable(),
});

async function getTodos(req, res, next) {
  try {
    const { filter = 'all' } = req.query;

    const where = {
      userId: req.user.id,
      ...(filter === 'active'    && { completed: false }),
      ...(filter === 'completed' && { completed: true  }),
    };

    const todos = await prisma.todo.findMany({
      where,
      orderBy: [
        { completed: 'asc' },
        { priority: 'desc' },
        { createdAt: 'desc' },
      ],
    });

    res.json(todos);
  } catch (err) { next(err); }
}

async function createTodo(req, res, next) {
  try {
    const parsed = todoSchema.safeParse(req.body);
    if (!parsed.success) {
      return res.status(400).json({ errors: parsed.error.flatten().fieldErrors });
    }

    const todo = await prisma.todo.create({
      data: { ...parsed.data, userId: req.user.id },
    });

    res.status(201).json(todo);
  } catch (err) { next(err); }
}

async function updateTodo(req, res, next) {
  try {
    const id = parseInt(req.params.id);
    
    // Ensure todo belongs to user
    const todo = await prisma.todo.findFirst({
      where: { id, userId: req.user.id },
    });
    if (!todo) return res.status(404).json({ error: 'Todo không tìm thấy' });

    const updated = await prisma.todo.update({
      where: { id },
      data:  req.body,
    });

    res.json(updated);
  } catch (err) { next(err); }
}

async function deleteTodo(req, res, next) {
  try {
    const id = parseInt(req.params.id);
    await prisma.todo.deleteMany({
      where: { id, userId: req.user.id }, // deleteMany vì findFirst + delete = 2 queries
    });
    res.status(204).send();
  } catch (err) { next(err); }
}

module.exports = { getTodos, createTodo, updateTodo, deleteTodo };

Frontend: React Todo App

javascript — src/api/todoApi.js
const BASE = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';

function getToken() {
  return localStorage.getItem('auth_token');
}

async function apiRequest(path, options = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
      ...options.headers,
    },
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({ error: 'Network error' }));
    throw new Error(err.error || `HTTP ${res.status}`);
  }

  return res.status === 204 ? null : res.json();
}

export const todoApi = {
  getAll:  (filter) => apiRequest(`/todos?filter=${filter}`),
  create:  (data)   => apiRequest('/todos', { method: 'POST', body: JSON.stringify(data) }),
  update:  (id, data) => apiRequest(`/todos/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
  remove:  (id)     => apiRequest(`/todos/${id}`, { method: 'DELETE' }),
};
jsx — hooks/useTodos.js
import { useState, useEffect, useCallback } from 'react';
import { todoApi } from '../api/todoApi';

export function useTodos(filter = 'all') {
  const [todos,   setTodos]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  const loadTodos = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const data = await todoApi.getAll(filter);
      setTodos(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [filter]);

  useEffect(() => { loadTodos(); }, [loadTodos]);

  const addTodo = async (todoData) => {
    const newTodo = await todoApi.create(todoData);
    setTodos(prev => [newTodo, ...prev]);
  };

  const toggleTodo = async (id) => {
    const todo = todos.find(t => t.id === id);
    const updated = await todoApi.update(id, { completed: !todo.completed });
    setTodos(prev => prev.map(t => t.id === id ? updated : t));
  };

  const deleteTodo = async (id) => {
    await todoApi.remove(id);
    setTodos(prev => prev.filter(t => t.id !== id));
  };

  return { todos, loading, error, addTodo, toggleTodo, deleteTodo };
}

3

Bài 8.3 — Deploy Full-stack App

Architecture khi deploy

Frontend → Vercel

Vercel tối ưu cho React/Vite. Free plan đủ dùng. Global CDN, HTTPS tự động, CI/CD từ GitHub.

Backend → Railway

Railway là PaaS đơn giản nhất cho Node.js. Free tier 500h/tháng. Database PostgreSQL tích hợp.

Database → Railway PostgreSQL

Chuyển từ SQLite (development) sang PostgreSQL (production) chỉ cần đổi 1 dòng trong schema.prisma.

Deploy Backend lên Railway

bash
# 1. Cài Railway CLI
npm install -g @railway/cli

# 2. Login và init
railway login
railway init   # Tạo project mới trên Railway

# 3. Thêm PostgreSQL database
railway add    # Chọn PostgreSQL

# 4. Set environment variables
railway variables set JWT_SECRET=your_super_secret_key_here
railway variables set NODE_ENV=production
railway variables set ALLOWED_ORIGIN=https://your-frontend.vercel.app

# 5. Deploy
railway up

# 6. Xem logs
railway logs

Chuyển đổi Database: SQLite → PostgreSQL

prisma — schema.prisma (production)
datasource db {
  provider = "postgresql"  // Đổi từ "sqlite"
  url      = env("DATABASE_URL")  // Railway cung cấp tự động
}

// Tất cả models giữ nguyên — Prisma handle migration!
bash — Run migrations trên production
# Add vào package.json scripts:
# "postinstall": "prisma generate && prisma migrate deploy"

# Hoặc chạy thủ công:
railway run npx prisma migrate deploy

Deploy Frontend lên Vercel

bash
# 1. Cài Vercel CLI
npm install -g vercel

# 2. Deploy (từ thư mục frontend/)
cd frontend
vercel

# 3. Set environment variable
vercel env add VITE_API_URL
# Nhập: https://your-backend.railway.app/api

# 4. Redeploy
vercel --prod

4

Bài 8.4 — Kết Nối Frontend + Backend Và Deploy Hoàn Chỉnh

Trong bài này chúng ta sẽ kết nối React frontend (từ Chương 6) với Express backend (từ Chương 7) thành một ứng dụng full-stack hoàn chỉnh, sau đó deploy lên internet.

Bước 1 — Cấu Hình Mono-repo Structure

bash — Tổ chức project full-stack
# Tạo thư mục gốc chứa cả frontend và backend
mkdir notes-fullstack
cd notes-fullstack

# Clone hoặc copy 2 project vào đây
# Đặt tên: frontend/ và backend/
mkdir frontend backend

# Tạo package.json ở root để chạy cả 2 cùng lúc
npm init -y
npm install --save-dev concurrently

# Cấu trúc cuối cùng:
# notes-fullstack/
# ├── frontend/        ← React + Vite app (từ Ch6)
# ├── backend/         ← Express API (từ Ch7)
# └── package.json     ← Root scripts
json — package.json ở root (thêm scripts)
{
  "name": "notes-fullstack",
  "scripts": {
    "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
    "dev:backend": "npm run dev --prefix backend",
    "dev:frontend": "npm run dev --prefix frontend",
    "build": "npm run build --prefix frontend"
  },
  "devDependencies": {
    "concurrently": "^8.0.0"
  }
}
bash — Chạy cả 2 service
# Chạy cả frontend và backend cùng lúc
npm run dev
# Backend chạy tại: http://localhost:3000
# Frontend chạy tại: http://localhost:5173

Bước 2 — Cấu Hình Proxy Để Tránh CORS

javascript — frontend/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // Chuyển hướng mọi request /api/* đến backend
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        // Không cần rewrite vì backend cũng dùng /api prefix
      }
    }
  }
});

Sau khi cấu hình proxy, frontend có thể gọi fetch('/api/notes') thay vì fetch('http://localhost:3000/api/notes') — Vite sẽ tự động forward.

Bước 3 — Tạo API Service Layer Trong React

javascript — frontend/src/services/api.js
// frontend/src/services/api.js
// Centralize tất cả API calls — dễ maintain và test

const BASE_URL = '/api';

function getAuthHeaders() {
  const token = localStorage.getItem('token');
  return token ? { Authorization: `Bearer ${token}` } : {};
}

async function apiRequest(endpoint, options = {}) {
  const response = await fetch(`${BASE_URL}${endpoint}`, {
    headers: {
      'Content-Type': 'application/json',
      ...getAuthHeaders(),
      ...options.headers,
    },
    ...options,
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.error || `HTTP ${response.status}`);
  }

  return data;
}

// Auth API
export const authAPI = {
  register: (email, password, name) =>
    apiRequest('/auth/register', { method: 'POST', body: JSON.stringify({ email, password, name }) }),

  login: (email, password) =>
    apiRequest('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }),
};

// Notes API
export const notesAPI = {
  getAll: () => apiRequest('/notes'),

  create: (title, content, pinned = false) =>
    apiRequest('/notes', { method: 'POST', body: JSON.stringify({ title, content, pinned }) }),

  update: (id, updates) =>
    apiRequest(`/notes/${id}`, { method: 'PUT', body: JSON.stringify(updates) }),

  delete: (id) =>
    apiRequest(`/notes/${id}`, { method: 'DELETE' }),
};

Bước 4 — Deploy Backend Lên Railway.app

bash — Deploy backend
# Cài Railway CLI
npm install -g @railway/cli

# Đăng nhập Railway (tạo tài khoản miễn phí tại railway.app)
railway login

# Khởi tạo project Railway trong thư mục backend/
cd backend
railway init

# Tạo file Procfile để Railway biết cách chạy app
echo "web: node src/index.js" > Procfile

# Thêm biến môi trường production lên Railway
railway variables set JWT_SECRET="$(node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))')"
railway variables set NODE_ENV=production

# Cho PostgreSQL (Railway cung cấp free PostgreSQL)
# Trong Railway dashboard: Add service → PostgreSQL
# Copy DATABASE_URL từ dashboard và set:
railway variables set DATABASE_URL="postgresql://..."

# Deploy lên Railway
railway up

# Lấy URL production
railway domain
# Output: https://your-app.up.railway.app

Bước 5 — Deploy Frontend Lên Vercel

bash — Deploy frontend
cd frontend

# Cập nhật BASE_URL trong api.js thành URL production của backend
# Hoặc dùng environment variable (khuyến nghị):

# Tạo file .env.production
echo 'VITE_API_URL=https://your-app.up.railway.app/api' > .env.production

# Cập nhật api.js dùng env var:
# const BASE_URL = import.meta.env.VITE_API_URL || '/api';

# Build cho production
npm run build

# Cài Vercel CLI và deploy
npm install -g vercel
vercel --prod

# Sau khi deploy xong, Vercel sẽ cho URL:
# https://your-app.vercel.app

# Cấu hình environment variable trên Vercel:
vercel env add VITE_API_URL production
# Nhập giá trị: https://your-app.up.railway.app/api

Bước 6 — Kiểm Tra Production

bash — Verify production deployment
# Test backend production
curl https://your-app.up.railway.app/health
# Kết quả mong đợi: {"status":"ok","time":"..."}

# Test đăng ký user qua production API
curl -X POST https://your-app.up.railway.app/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"prod@test.com","password":"TestPass123","name":"Test User"}'

# Mở frontend production trên trình duyệt
# https://your-app.vercel.app
# Đăng ký → Đăng nhập → Tạo ghi chú → Kiểm tra dữ liệu persist
🚀
Full-stack App Đã Live!

Ứng dụng của bạn đang chạy trên internet với backend Railway + frontend Vercel + database PostgreSQL. Đây là stack production thực sự — cùng kiến trúc mà nhiều startup đang dùng.

💡 Mẹo từ ThanhDoIT
  • Khi làm full-stack, hãy define API contract (endpoints, request/response types) trước khi code cả frontend lẫn backend. Dùng AI để generate contract từ user stories.
  • Environment variables: VITE_ prefix cho frontend (public), không prefix cho backend (private). KHÔNG put secrets vào VITE_ variables — nó expose ra bundle!
  • Trong development, dùng proxy của Vite (vite.config.js) để avoid CORS issues thay vì tắt CORS trên backend. Đây là cách đúng nhất.
  • Trước khi deploy production, chạy Lighthouse audit trên Chrome DevTools. AI có thể fix hầu hết các performance issues Lighthouse tìm ra nếu bạn paste kết quả vào.

5

Bài 8.5 — CI/CD Pipeline & Production Checklist

Deploy một lần thì ai cũng làm được — nhưng deploy đúng cách với automation, testing, và monitoring thì mới là professional. Trong bài này bạn sẽ setup CI/CD pipeline hoàn chỉnh và checklist production-ready.

🔄
CI/CD Flow
  • Push code → GitHub Actions trigger
  • CI: lint + test + build
  • CD: auto-deploy khi pass
  • Preview deployments cho PRs
Production Checklist
  • Environment variables secure
  • Database migrations ready
  • Error monitoring (Sentry)
  • Performance: bundle size, images

GitHub Actions CI/CD cho Full-stack App

yaml — .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # === CI: Test & Build ===
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run typecheck

      - name: Run tests
        run: npm test -- --coverage
        env:
          DATABASE_URL: "file:./test.db"
          JWT_SECRET: "test-secret-for-ci"

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

      - name: Build
        run: npm run build

  # === CD: Deploy (chỉ khi push vào main) ===
  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Railway
        run: |
          npm install -g @railway/cli
          railway up --service my-backend
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

      - name: Run DB migrations
        run: railway run npx prisma migrate deploy
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
Tôi có full-stack app: React frontend (Vite) + Express backend (TypeScript).
Hãy tạo GitHub Actions workflow CI/CD hoàn chỉnh:

Requirements:
1. CI: chạy lint + typecheck + tests cho cả frontend và backend
2. Caching: npm cache để speed up pipeline
3. Matrix strategy: test trên Node 18 và Node 20
4. CD: deploy frontend lên Vercel, backend lên Railway
5. Environment secrets: DATABASE_URL, JWT_SECRET, RAILWAY_TOKEN, VERCEL_TOKEN
6. Chỉ deploy khi push vào main branch (không deploy khi tạo PR)
7. Slack notification khi deploy success/fail

Giải thích mỗi step tại sao cần thiết.
CI/CD là một trong những chủ đề Copilot giỏi nhất — nó có đầy đủ context về GitHub Actions syntax và best practices.

Pre-deploy Checklist — Không Deploy Khi Chưa Check

CategoryCheckCommand
SecurityKhông có secrets trong codegit log --all -p | grep -i "password\|secret\|token"
Security.env không bị commitgit status && cat .gitignore
TestsTất cả tests passnpm test
BuildBuild thành công, không warningnpm run build
TypesNo TypeScript errorsnpm run typecheck
DBMigrations sẵn sàngnpx prisma migrate status
PerfBundle size acceptablenpx vite-bundle-analyzer
EnvEnv vars production có đủKiểm tra Railway/Vercel dashboard
HealthHealth endpoint hoạt độngcurl https://your-api.up.railway.app/health

Monitoring & Error Tracking với Sentry

typescript — sentry.setup.ts
// npm install @sentry/node @sentry/profiling-node
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

export function initSentry(app: express.Application) {
  if (process.env.NODE_ENV !== 'production') return;

  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    integrations: [
      Sentry.httpIntegration(),
      Sentry.expressIntegration(),
      nodeProfilingIntegration(),
    ],
    tracesSampleRate: 0.1,     // Sample 10% của requests
    profilesSampleRate: 0.1,
    environment: process.env.NODE_ENV,
  });

  // Đặt TRƯỚC tất cả routes
  app.use(Sentry.expressErrorHandler());
}

// Trong error handler:
// Sentry.captureException(err);
// Mỗi error trong production sẽ notify qua email/Slack
💡 Sentry Free Tier
  • Sentry free: 5,000 errors/tháng — đủ cho project học tập và small production apps
  • Tự động capture unhandled exceptions, performance issues, user context
  • Có thể dùng AI của Sentry để suggest fixes cho errors: "Fix with AI" button
  • Alternative miễn phí: LogRocket (frontend), Highlight.io (full-stack)
🎯 Thực Hành: Deploy với CI/CD
  1. Tạo .github/workflows/ci.yml — chạy lint + test khi push
  2. Hỏi Copilot: "Generate GitHub Actions workflow cho Express + Prisma app deploy lên Railway"
  3. Setup GitHub Secrets: RAILWAY_TOKEN, DATABASE_URL
  4. Push code → xem Actions tab chạy CI pipeline
  5. Cố tình để 1 test fail → confirm pipeline block deploy
  6. Fix test → push → confirm auto-deploy thành công
🎯 Bài Tập Tổng Kết Chương 8 — Full-stack Production App
  1. Lên kế hoạch với AI: user stories → database schema → API contract → component list
  2. Build full-stack app: React frontend + Express backend + PostgreSQL
  3. Deploy: backend lên Railway với PostgreSQL, frontend lên Vercel
  4. Setup CI/CD: GitHub Actions chạy test + auto-deploy khi push main
  5. Add error monitoring: Sentry hoặc Highlight.io
  6. Thử thách: Thêm real-time feature bằng WebSocket (Socket.io) — hỏi Copilot để implement
⚠️ 5 Sai Lầm Phổ Biến Khi Deploy Full-stack
  • Commit .env vào Git: Lỗi nguy hiểm nhất — JWT_SECRET, DATABASE_URL, API keys bị lộ công khai. Thêm .env vào .gitignore ngay từ dòng đầu tiên.
  • Không set NODE_ENV=production: Nhiều middleware (Helmet, Morgan, rate limiter) có behavior khác nhau giữa dev và production. Đặt sai → app deploy nhưng hoạt động kỳ lạ.
  • Database connection pool không đủ: Free tier Railway/Render thường giới hạn connections. Pool=2-3 là đủ cho free tier; pool quá cao gây OOM.
  • Thiếu health check endpoint: Railway/Render cần GET /health để biết app đang chạy. Thiếu → deploy thất bại hoặc restart liên tục.
  • CORS quá rộng trong production: origin: '*' ổn cho dev, nguy hiểm cho production. Phải set đúng domain: origin: 'https://yourapp.vercel.app'.
✅ Checklist Deploy Thành Công
  • Backend URL trả về JSON khi gọi GET /health từ trình duyệt
  • Frontend load được và gọi API thành công — xem Network tab trong DevTools
  • Đăng ký tài khoản mới → login → CRUD data → data persist sau refresh
  • GitHub Actions pipeline xanh ✅ — test pass, deploy thành công
  • Không có .env nào được commit vào Git (check: git log --all -- .env)
  • Error monitoring (Sentry/Highlight.io) nhận được test event

🗒 Tóm Tắt Chương 8

  • Luôn lên kế hoạch trước khi code — dùng AI để tạo spec, schema, API contract trong 30 phút
  • Monorepo (1 repo, 2 folder) giúp quản lý full-stack project dễ hơn nhiều
  • Custom hooks tách business logic khỏi UI, API layer riêng dễ switch environment
  • Vercel cho frontend, Railway cho backend — combo miễn phí mạnh nhất hiện tại
  • CI/CD: GitHub Actions tự động lint + test + deploy khi push main — không deploy thủ công
  • Sentry/Highlight.io: error monitoring production — biết ngay khi user gặp lỗi
Zalo: 0898 619 966 Z Gọi: 0898 619 966