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.
🎯 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
Bài 8.1 — Lập Kế Hoạch Dự Án với AI
.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.
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.
AI tạo Database Schema
Paste user stories, nhờ AI thiết kế Prisma schema. Review kỹ relationships, indexes, và constraints.
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.
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 đủ.
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
Bài 8.2 — Dự Án 1: Todo App Full-stack
Quy Trình Full-Stack Project từ Đầu
Paste user stories vào Copilot, nhờ AI thiết kế API endpoints. Output: danh sách đầy đủ method + path + body + response.
Từ API contract, AI thiết kế Prisma schema. Review relationships và indexes. Run npx prisma migrate dev.
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.
List tất cả components cần có. Tách thành: Page components, Feature components, UI components. AI generate component structure.
Tạo API layer (src/api/), custom hooks để fetch data. Xử lý loading, error states. Test với backend thật chạy local.
Backend lên Railway/Render, Frontend lên Vercel. Setup GitHub Actions để auto-deploy khi push main. Add health check endpoint.
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
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
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;
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
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' }),
};
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 };
}
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
# 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
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!
# 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
# 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
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
# 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
{
"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"
}
}
# 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
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
// 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
# 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
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
# 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
Ứ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.
- 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.
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.
- Push code → GitHub Actions trigger
- CI: lint + test + build
- CD: auto-deploy khi pass
- Preview deployments cho PRs
- Environment variables secure
- Database migrations ready
- Error monitoring (Sentry)
- Performance: bundle size, images
GitHub Actions CI/CD cho Full-stack App
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.
Pre-deploy Checklist — Không Deploy Khi Chưa Check
| Category | Check | Command |
|---|---|---|
| Security | Không có secrets trong code | git log --all -p | grep -i "password\|secret\|token" |
| Security | .env không bị commit | git status && cat .gitignore |
| Tests | Tất cả tests pass | npm test |
| Build | Build thành công, không warning | npm run build |
| Types | No TypeScript errors | npm run typecheck |
| DB | Migrations sẵn sàng | npx prisma migrate status |
| Perf | Bundle size acceptable | npx vite-bundle-analyzer |
| Env | Env vars production có đủ | Kiểm tra Railway/Vercel dashboard |
| Health | Health endpoint hoạt động | curl https://your-api.up.railway.app/health |
Monitoring & Error Tracking với Sentry
// 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: 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)
- Tạo
.github/workflows/ci.yml— chạy lint + test khi push - Hỏi Copilot: "Generate GitHub Actions workflow cho Express + Prisma app deploy lên Railway"
- Setup GitHub Secrets:
RAILWAY_TOKEN,DATABASE_URL - Push code → xem Actions tab chạy CI pipeline
- Cố tình để 1 test fail → confirm pipeline block deploy
- Fix test → push → confirm auto-deploy thành công
- Lên kế hoạch với AI: user stories → database schema → API contract → component list
- Build full-stack app: React frontend + Express backend + PostgreSQL
- Deploy: backend lên Railway với PostgreSQL, frontend lên Vercel
- Setup CI/CD: GitHub Actions chạy test + auto-deploy khi push main
- Add error monitoring: Sentry hoặc Highlight.io
- Thử thách: Thêm real-time feature bằng WebSocket (Socket.io) — hỏi Copilot để implement
- 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
.envvào.gitignorengay 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'.
- Backend URL trả về JSON khi gọi
GET /healthtừ 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ó
.envnà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