Skip to content

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

  1. Thiết kế REST API chuyên nghiệp (REST conventions, status codes, error format)
  2. Database schema design với Prisma ORM
  3. JWT Authentication (access + refresh token)
  4. Middleware pattern (auth, validation, error handling)
  5. 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=20

Response 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

CodeMeaningWhen
200OKGET success, UPDATE success
201CreatedPOST success
204No ContentDELETE success
400Bad RequestValidation error
401UnauthorizedMissing/invalid token
403ForbiddenValid token, no permission
404Not FoundResource doesn't exist
409ConflictDuplicate (email exists)
422UnprocessableBusiness rule violation
500Internal Server ErrorBug (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 refreshToken

Implementation

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.service

Controller 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

ComponentKey Pattern
REST URLsResource-based, versioned, nested
DatabasePrisma, indexes, soft delete, audit fields
AuthJWT access (15m) + refresh (7d)
ArchitectureRoutes → Controllers → Services → DB
ErrorsCentralized AppError + error middleware

⏭️ Buổi tiếp theo

Buổi 10: Frontend Modern — React/Vue + State Management 🎨

Powered by CodyMaster × VitePress