Buổi 11: Full-Stack Integration — API ↔ Frontend 🔗
Thành quả: Blog app chạy end-to-end: login → create post → read → update → delete
🎯 Mục Tiêu
- Connect frontend ↔ backend: CORS, environment variables
- Loading states, error handling, optimistic updates
- File upload flow (avatar, post images)
- WebSocket basics (real-time notifications)
- cm-execution mode: batch → parallel
📖 Phần 1: Integration Checklist
Connection Setup
typescript
// Backend: Enable CORS
import cors from 'cors';
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Frontend: Environment variables
// .env.development
VITE_API_URL=http://localhost:3000/api/v1
// .env.production
VITE_API_URL=https://api.myapp.com/api/v1Integration Verification Script
bash
#!/bin/bash
# verify-integration.sh
echo "🔍 Checking backend..."
curl -s http://localhost:3000/api/v1/health | jq .
echo ""
echo "🔍 Testing auth flow..."
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"password123"}' \
| jq -r '.accessToken')
echo "Token: ${TOKEN:0:20}..."
echo "🔍 Testing protected endpoint..."
curl -s http://localhost:3000/api/v1/posts \
-H "Authorization: Bearer $TOKEN" | jq '.meta'
echo "✅ Integration OK!"📖 Phần 2: Loading & Error States
Pattern: 3 States Component
tsx
// Every data component handles 3 states:
function PostList() {
const { data, isLoading, isError, error, refetch } = usePosts();
// State 1: Loading
if (isLoading) {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="skeleton h-48 rounded-lg animate-pulse bg-gray-200" />
))}
</div>
);
}
// State 2: Error
if (isError) {
return (
<div className="error-state p-8 text-center">
<AlertCircle className="mx-auto mb-4 text-red-500" size={48} />
<h3>Failed to load posts</h3>
<p className="text-gray-500">{error.message}</p>
<button onClick={() => refetch()} className="btn mt-4">
Try Again
</button>
</div>
);
}
// State 3: Empty
if (!data?.data.length) {
return (
<div className="empty-state p-8 text-center">
<FileText className="mx-auto mb-4 text-gray-400" size={48} />
<h3>No posts yet</h3>
<Link to="/posts/new" className="btn btn-primary mt-4">
Create First Post
</Link>
</div>
);
}
// State 4: Success (data)
return (
<div className="grid grid-cols-3 gap-4">
{data.data.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}Optimistic Updates
typescript
export function useDeletePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: postService.delete,
// Optimistic: remove from list immediately
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previous = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old: any) => ({
...old,
data: old.data.filter((p: Post) => p.id !== postId),
}));
return { previous };
},
// Rollback on error
onError: (err, postId, context) => {
queryClient.setQueryData(['posts'], context?.previous);
toast.error('Failed to delete post');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}📖 Phần 3: File Upload
Backend: Multer Setup
typescript
// middleware/upload.middleware.ts
import multer from 'multer';
import path from 'path';
const storage = multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
},
});
const fileFilter = (req: any, file: Express.Multer.File, cb: any) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (allowed.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new AppError('Only JPEG, PNG, and WebP images allowed', 400), false);
}
};
export const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});
// Route
router.post('/upload', authenticate, upload.single('image'), uploadController.handleUpload);Frontend: Upload Component
tsx
function ImageUpload({ onUpload }: { onUpload: (url: string) => void }) {
const [preview, setPreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const handleFile = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Preview
setPreview(URL.createObjectURL(file));
// Upload
setUploading(true);
const formData = new FormData();
formData.append('image', file);
try {
const { data } = await api.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
onUpload(data.url);
toast.success('Image uploaded!');
} catch {
toast.error('Upload failed');
} finally {
setUploading(false);
}
};
return (
<div className="upload-zone">
<input type="file" accept="image/*" onChange={handleFile} />
{preview && <img src={preview} alt="Preview" className="preview" />}
{uploading && <Spinner />}
</div>
);
}📖 Phần 4: Environment & Deployment Config
project/
├── backend/
│ ├── .env.development # DATABASE_URL=localhost, JWT_SECRET=dev
│ ├── .env.production # ⚠️ Never commit!
│ └── .env.example # Template for new devs
├── frontend/
│ ├── .env.development # VITE_API_URL=http://localhost:3000
│ ├── .env.production # VITE_API_URL=https://api.prod.com
│ └── .env.example
└── docker-compose.yml # PostgreSQL + Redis localDocker Compose for Local Dev
yaml
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: blogapp
POSTGRES_USER: dev
POSTGRES_PASSWORD: devpass
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:📖 Phần 5: cm-execution Modes
| Mode | Use Case | Speed |
|---|---|---|
| Sequential | Dependent tasks (auth → CRUD → test) | Normal |
| Batch | Group of related small tasks | 2x |
| Parallel | Independent tasks (frontend + backend) | 3x |
# Parallel execution example
cm-execution --mode parallel \
--task-1 "Generate backend API for comments" \
--task-2 "Generate frontend PostDetail component" \
--task-3 "Generate test suite for post service"🧪 Lab: End-to-End Integration
Task: Wire it all together (90 min)
Step 1: Start backend + database
docker-compose up -d
cd backend && npm run dev
Step 2: Start frontend
cd frontend && npm run dev
Step 3: Test full flow
→ Register user → Login → Create post → View post
→ Edit post → Delete post → Logout → Verify redirect
Step 4: Error scenarios
→ Submit invalid form → see validation errors
→ Access protected page without login → redirect
→ Make API call with expired token → auto-refresh
→ Upload too-large file → see error message
Step 5: Screenshot evidence
→ Capture each flow step
→ Document in integration-test.md🎓 Tóm Tắt
| Integration Layer | Pattern |
|---|---|
| Connection | CORS + env vars + health check |
| Loading | 3 states: loading, error, empty, data |
| Optimistic | Update UI first, rollback on error |
| File Upload | Multer backend + FormData frontend |
| Dev Setup | Docker Compose for local services |
⏭️ Buổi tiếp theo
Buổi 12: Database & Performance — Query Optimization 📊