Follow & Friend Request

user · follow / friend
Module: UserService · FollowRepository · FriendRequestRepository  ·  DB: Follow · InvitationRequest (có sẵn)
1
API Follow
POST /user/:username/follower
0
API Unfollow
dùng chung toggle
0
Friend Request
chưa implement
Follow
Bảng DB
có sẵn trong Prisma

Current State


POST /user/:username/follower — toggle follow/unfollow. Kiểm tra self-follow, tìm record cũ, flip isFollowing, cập nhật followersCount.
Bảng Follow trong Prisma: followerId, followingId, isFollowing. Bảng InvitationRequest cũng tồn tại (cho Circle).
Thiếu
Không có API lấy danh sách following (người mình follow). Chỉ có getFollower (người follow mình).
Thiếu
Không có friend request (gửi/chấp nhận/từ chối). Hệ thống hiện là follow 1 chiều như Twitter, chưa có mutual friend.
Ghi chú
Follow và friend request đang đi qua User module; chưa tách module riêng cho phần quan hệ xã hội.

Follow / Unfollow Flow (implemented)


POST /user/:username/follower Self follow? yes return false no Record cũ? có + đang follow updateStatus(false) decrementFollowersCount có + chưa follow updateStatus(true) incrementFollowersCount chưa có create(followerId, followingId) incrementFollowersCount

Toggle pattern: 1 endpoint xử lý cả follow và unfollow. Nếu đã follow → unfollow, ngược lại → follow.

Implementation Checklist


#TaskChi tiết triển khaiStatus
01
Follow / Unfollow (toggle)
POST /user/:username/follower
✅ Đã có. Toggle isFollowing trên bảng Follow. Cập nhật followersCount trên User. Chặn self-follow. done
02
GET danh sách followers
GET /user/:username/followers
✅ Đã hoàn thành. Route + controller method sử dụng username params. Cursor pagination. done
03
GET danh sách following
GET /user/:username/following
✅ Đã hoàn thành. Filter where: { followerId: userId }. Cursor pagination. done
04
Bảng FriendRequest
Prisma migration
✅ Đã thêm model FriendRequest vào schema. Prisma client đã được generate. done
05
Gửi lời mời kết bạn
POST /user/:username/friend-request
✅ Đã hoàn thành (chưa có realtime notification). Kiểm tra người dùng tồn tại, tạo record status=PENDING. done
06
Chấp nhận / Hủy lời mời
PATCH /me/friend-requests/:username/accept
PATCH /me/friend-requests/:username/cancel
✅ Đã hoàn thành (chưa auto follow & realtime). Tách riêng 2 endpoint cho rõ ràng logic route. Cập nhật trạng thái ACCEPTED / CANCELLED sử dụng Transaction. done
07
Danh sách lời mời
GET /me/friend-requests/received
GET /me/friend-requests/sent
✅ Đã hoàn thành. Lấy danh sách lời mời PENDING. Cursor pagination. Có include sender/receiver info (name, avatar, username). done
08
Check mutual friend
Helper function
Hàm isMutualFriend(userA, userB): check cả 2 có follow lẫn nhau hoặc có FriendRequest status=ACCEPTED. Dùng cho feed filter FRIENDS_ONLY. todo
09
Realtime: Có người Follow
Pusher Event
Bắn event Pusher qua channel private-user-{userId} khi một user nhấn Follow. Gửi thông tin người follow để UI hiển thị Toast. todo
10
Realtime: Nhận Friend Request
Pusher Event
Bắn event báo có lời mời kết bạn mới khi gọi API POST /user/:username/friend-request. Cập nhật con số (badge) trên màn hình Frontend. todo
11
Realtime: Accept Friend Request
Pusher Event
Bắn event thông báo cho người gửi biết lời mời của họ đã được chấp nhận khi gọi API PATCH /me/friend-requests/:username. todo
12
Lưu Notification vào DB
Prisma Schema
Thiết kế bảng Notification để lưu lịch sử thông báo (để user xem lại khi offline). Các hành động trên sẽ vừa bắn event realtime vừa insert vào bảng này. todo

Friend Request Route Flow


Client Request PATCH /.../friend-requests/:user/cancel PATCH /.../friend-requests/:user/accept Middleware Validation validate(usernameParamsSchema) validate(friendRequestSchema) Controller & Service handleFriendRequestCancel() handleFriendRequestAccept()

Luồng Route mới: Thay vì xử lý Accept/Reject chung 1 endpoint, API đã tách biệt rõ ràng thành 2 endpoint độc lập /cancel/accept. Việc này giúp tuân thủ SRP (Single Responsibility Principle) và thuận tiện cho việc mở rộng logic hoặc phân quyền sau này.

Cursor Pagination Flow (Friend Requests)


GET /me/friend-requests/received ?after=userId_ABC Backend Controller 1. Lấy receiverId từ Token (User Login) 2. Lấy senderId từ query 'after' Prisma Query cursor: { senderId_receiverId } Return Data & Next 'after' Tại sao không dùng Request ID? Để bảo mật, tránh lộ ID tự tăng. Dùng cặp ID (người gửi, người nhận) làm unique cursor trong database.

Luồng Cursor Pagination an toàn: Frontend chỉ truyền ID của user (sender/receiver), Backend tự ghép thành Composite Key để truy vấn trang tiếp theo. (Tương tự với danh sách "Sent" nhưng tráo đổi vị trí ID).

Potential Issues


Race cond.
2 request follow đồng thời có thể tạo 2 record Follow. Giải pháp: unique constraint (followerId, followingId) trong DB + upsert thay vì check-then-create.
Count drift
followersCount trên User có thể sai nếu increment/decrement fail sau khi update Follow. Giải pháp: wrap trong transaction hoặc dùng background job recount định kỳ.
Friend vs Follow
Hệ thống hiện là follow 1 chiều. Thêm friend request tạo 2 model quan hệ. Cần quyết định: friend = mutual follow hay friend = accepted request? Khuyến nghị: friend = accepted request, auto follow khi accept.
Spam
User có thể spam follow/unfollow liên tục. Cần rate limit (ví dụ max 30 follow/phút) dùng Redis sliding window hoặc bảng AntiSpam có sẵn.
Feed impact
Khi accept friend → feed FRIENDS_ONLY cần hiện bài user mới. Nếu dùng fan-out-on-write, cần enqueue bài cũ vào feed mới. Nếu fan-out-on-read thì chỉ cần update social graph cache.
Transaction
Chấp nhận / Hủy lời mời (PATCH): Tách làm 2 hàm riêng handleFriendRequestAccepthandleFriendRequestCancel, nhưng cả 2 đều sử dụng Prisma Transaction (transactionService.doInTransaction). Chuỗi hành động: Tìm ID đối tượng từ username → Kiểm tra lời mời có tồn tại không qua cặp (senderId, receiverId) tương ứng → Cập nhật trạng thái thành ACCEPTED hoặc CANCELLED. Việc tách 2 route giúp làm rõ logic module route. Toàn bộ được bọc trong transaction để dễ rollback khi lỗi.
Pagination
Lưu ý CỰC KỲ QUAN TRỌNG về biến after (Cursor Pagination) cho Friend Requests:
Hệ thống thiết kế KHÔNG DÙNG ID tự tăng (Auto-increment ID) của bảng FriendRequest làm cursor nhằm bảo mật (tránh bị ID enumeration/cào data). Thay vào đó, cursor là ID của User trong cặp quan hệ:
  • GET /me/friend-requests/received: (Lời mời bạn nhận được). Giá trị after phải là ID của người gửi (senderId) của record cuối cùng ở trang hiện tại.
  • GET /me/friend-requests/sent: (Lời mời bạn đã gửi). Giá trị after phải là ID của người nhận (receiverId) của record cuối cùng ở trang hiện tại.
Khi nhận được after, Backend sẽ tự động dùng nó kết hợp với ID của người đang đăng nhập để tạo ra composite cursor { senderId_receiverId } query trực tiếp vào Database rất nhanh và an toàn. Frontend Developer cần chú ý truyền đúng giá trị này khi làm tính năng "Load more".

Tech Stack


LayerCông nghệGhi chú
DatabaseMySQL (Prisma)Bảng Follow có sẵn. Cần thêm FriendRequest. Unique constraints cho idempotent.
APIExpress Router + Zod validateValidate params (username), body (action). Authorization middleware có sẵn.
RealtimePusher (Soketi)Channel private-user-{userId}. Event: friend-request:new, friend-request:accepted.
Anti-spamRedis sliding windowKey: follow-rate:{userId}. TTL 60s. Max 30 requests.