Buổi 09: Backend Foundation — REST API, Database, Authentication 🔧
Thành quả: Build REST API hoàn chỉnh: 5+ endpoints + JWT auth + PostgreSQL + Prisma
🎯 Mục Tiêu
- Thiết kế REST API chuyên nghiệp (REST conventions, status codes, error format)
- Database schema design với Prisma ORM
- JWT Authentication (access + refresh token)
- Middleware pattern (auth, validation, error handling)
- Dùng cm-planning + cm-execution workflow
📖 Phần 1: REST API Design Principles
URL Convention
GET /api/v1/users → List users (paginated)
GET /api/v1/users/:id → Get single user
POST /api/v1/users → Create user
PUT /api/v1/users/:id → Update user (full)
PATCH /api/v1/users/:id → Update user (partial)
DELETE /api/v1/users/:id → Delete user
# Nested resources
GET /api/v1/users/:id/posts → User's posts
POST /api/v1/users/:id/posts → Create post for user
# Filtering, sorting, pagination
GET /api/v1/posts?status=published&sort=-createdAt&page=2&limit=20Response Format
json
// Success
{
"success": true,
"data": { /* resource */ },
"meta": {
"page": 1,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
// Error
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{ "field": "email", "message": "Must be a valid email" }
]
}
}HTTP Status Codes
| Code | Meaning | When |
|---|---|---|
| 200 | OK | GET success, UPDATE success |
| 201 | Created | POST success |
| 204 | No Content | DELETE success |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Missing/invalid token |
| 403 | Forbidden | Valid token, no permission |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate (email exists) |
| 422 | Unprocessable | Business rule violation |
| 500 | Internal Server Error | Bug (never expose details) |
📖 Phần 2: Database Design với Prisma
Schema Design
prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String
password String
role Role @default(USER)
posts Post[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // Soft delete
@@map("users")
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
status PostStatus @default(DRAFT)
author User @relation(fields: [authorId], references: [id])
authorId String
tags Tag[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([status, createdAt])
@@map("posts")
}
model Comment {
id String @id @default(cuid())
content String
author User @relation(fields: [authorId], references: [id])
authorId String
post Post @relation(fields: [postId], references: [id])
postId String
createdAt DateTime @default(now())
@@index([postId])
@@map("comments")
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
@@map("tags")
}
enum Role {
USER
ADMIN
MODERATOR
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}Migration Commands
bash
npx prisma migrate dev --name init
npx prisma generate
npx prisma studio # Visual DB browser
npx prisma db seed # Seed data📖 Phần 3: JWT Authentication
Architecture
┌────────┐ POST /auth/login ┌──────────┐
│ Client │ ──── email + password ────→ │ Server │
│ │ ←── accessToken (15m) ──── │ │
│ │ ←── refreshToken (7d) ──── │ │
└────┬───┘ └──────────┘
│
│ GET /api/users (with Authorization: Bearer <accessToken>)
│
│ When accessToken expires:
│ POST /auth/refresh (with refreshToken)
│ ←── new accessToken + new refreshTokenImplementation
typescript
// services/auth.service.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
export class AuthService {
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) throw new AppError('Invalid credentials', 401);
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw new AppError('Invalid credentials', 401);
const accessToken = this.generateAccessToken(user.id, user.role);
const refreshToken = this.generateRefreshToken(user.id);
return { accessToken, refreshToken, user: this.sanitize(user) };
}
private generateAccessToken(userId: string, role: string): string {
return jwt.sign({ userId, role }, process.env.JWT_ACCESS_SECRET!, {
expiresIn: '15m',
});
}
private generateRefreshToken(userId: string): string {
return jwt.sign({ userId }, process.env.JWT_REFRESH_SECRET!, {
expiresIn: '7d',
});
}
private sanitize(user: User): Omit<User, 'password'> {
const { password, ...safe } = user;
return safe;
}
}Auth Middleware
typescript
// middleware/auth.middleware.ts
export const authenticate = asyncHandler(async (req, res, next) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
throw new AppError('No token provided', 401);
}
const token = header.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET!);
req.user = await prisma.user.findUnique({ where: { id: decoded.userId } });
if (!req.user) throw new AppError('User not found', 401);
next();
});
export const authorize = (...roles: Role[]) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
throw new AppError('Insufficient permissions', 403);
}
next();
};
};📖 Phần 4: Clean Architecture Layers
Routes → Controllers → Services → Repositories
(HTTP) (Request/Res) (Business) (Database)
routes/ controllers/ services/ prisma client
user.routes user.ctrl user.service + custom queries
post.routes post.ctrl post.service
auth.routes auth.ctrl auth.serviceController Example
typescript
// controllers/post.controller.ts
export const createPost = asyncHandler(async (req: AuthRequest, res: Response) => {
const validated = createPostSchema.parse(req.body);
const post = await postService.create({
...validated,
authorId: req.user!.id,
slug: slugify(validated.title),
});
res.status(201).json({ success: true, data: post });
});
export const getPosts = asyncHandler(async (req: Request, res: Response) => {
const { page = 1, limit = 20, status, tag, sort = '-createdAt' } = req.query;
const result = await postService.findAll({
page: Number(page),
limit: Number(limit),
status: status as PostStatus,
tag: tag as string,
sort: sort as string,
});
res.json({ success: true, ...result });
});📖 Phần 5: Error Handling
typescript
// utils/app-error.ts
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public code?: string,
public details?: any[]
) {
super(message);
this.code = code || this.getCodeFromStatus(statusCode);
}
private getCodeFromStatus(status: number): string {
const map: Record<number, string> = {
400: 'BAD_REQUEST',
401: 'UNAUTHORIZED',
403: 'FORBIDDEN',
404: 'NOT_FOUND',
409: 'CONFLICT',
422: 'UNPROCESSABLE',
};
return map[status] || 'INTERNAL_ERROR';
}
}
// middleware/error.middleware.ts
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: { code: err.code, message: err.message, details: err.details },
});
}
// Unexpected errors — log but don't expose
console.error('Unexpected error:', err);
res.status(500).json({
success: false,
error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' },
});
};🧪 Lab: Build Blog API
Task: Complete REST API (90 min, multi-session)
Phase 1: Setup (15 min)
→ npm init, install deps, Prisma schema, seed data
Phase 2: Auth (20 min) — cm-tdd
→ POST /auth/register
→ POST /auth/login
→ POST /auth/refresh
Phase 3: Posts CRUD (30 min) — cm-tdd
→ GET /posts (paginated, filter)
→ GET /posts/:slug
→ POST /posts (auth)
→ PUT /posts/:id (author only)
→ DELETE /posts/:id (author/admin)
Phase 4: Integration tests (25 min)
→ 15+ tests total
→ npm run test:coverage → 90%+🎓 Tóm Tắt
| Component | Key Pattern |
|---|---|
| REST URLs | Resource-based, versioned, nested |
| Database | Prisma, indexes, soft delete, audit fields |
| Auth | JWT access (15m) + refresh (7d) |
| Architecture | Routes → Controllers → Services → DB |
| Errors | Centralized AppError + error middleware |
⏭️ Buổi tiếp theo
Buổi 10: Frontend Modern — React/Vue + State Management 🎨