Server DTO: Nên giữ nguyên hay chuyển đổi?
Trong các dự án mà Backend và Frontend được phát triển hoàn toàn tách biệt, chắc hẳn không ít lần chúng ta phải đau đầu với câu hỏi: "Dữ liệu server trả về thì nên xài luôn hay phải xào nấu lại?".
Nếu dạo quanh các diễn đàn công nghệ hay đọc các bài thảo luận trên mạng, bạn sẽ thấy cuộc tranh luận về cách xử lý dữ liệu từ API này dường như chưa bao giờ có hồi kết. Cốt lõi của vấn đề luôn xoay quanh hai luồng quan điểm cực kỳ quen thuộc:
"Dữ liệu backend trả về sao thì cứ tận dụng nguyên xi như vậy là chuẩn nhất" vs. "Không được, Frontend phải có domain model của riêng mình chứ"
Cứ mỗi dự án mới, bài toán về cách xử lý DTO (Data Transfer Object) này lại được lôi ra mổ xẻ. Cá nhân tôi thì nghiêng về phía quan điểm thứ hai. Tuy nhiên, tôi không hề có ý khẳng định rằng cứ nhắm mắt đắp thêm một transformation layer (layer chuyển đổi) vào là giải quyết được mọi chuyện.
Trong bài viết hôm nay, tôi muốn chia sẻ những trải nghiệm thực tế của mình: Rốt cuộc thì trong tình huống nào việc phụ thuộc trực tiếp vào DTO sẽ trở thành một rủi ro tiềm ẩn, và khi nào thì chúng ta thực sự cần xây dựng một transformation layer.
Trong giai đoạn phát triển, DTO thay đổi rất thường xuyên
Tham gia các dự án thực tế, chắc hẳn các bạn đã từng gặp cảnh này rồi đúng không?
Tuần 1:

Tuần 2: Dev Backend: "À, tên được tách thành firstName và lastName rồi nhé"

Tuần 3:
Dev Backend: "Mình xóa trường email và gộp chung vào contactInfo rồi nha"

Mỗi lần có thay đổi như thế này, Frontend sẽ ra sao?

Các bạn thấy chứ? Lỗi sẽ nổ ra ở tất cả các component đang sử dụng trực tiếp DTO.
Trong giai đoạn phát triển, chuyện DTO thay đổi mỗi hai tuần một lần là rất bình thường, và ngay cả khi đã lên môi trường production (vận hành), nó vẫn có thể thay đổi thường xuyên tùy theo yêu cầu.
Đặc biệt trên Mobile App, lỗi này còn chí mạng hơn
Nếu là một Web developer, bạn có thể nghĩ: "API thay đổi thì cứ deploy lại ngay lập tức là xong chứ gì?". Đúng vậy. Vì Web có thể deploy một cách tự do nên chúng ta có thể đối phó với việc DTO thay đổi một cách tương đối linh hoạt.
Nhưng đối với Mobile App, đó lại là một câu chuyện hoàn toàn khác.
App mang tính "lưu lại vĩnh viễn"
Kể từ khoảnh khắc được phát hành lên App Store hay Play Store, ứng dụng đó sẽ lập tức vượt ra khỏi tầm kiểm soát của developer. Bạn không thể đảm bảo được khi nào người dùng sẽ bấm cập nhật, hoặc thậm chí là họ có thèm cập nhật hay không.

ấn đề trong tình huống này là:
- Vẫn còn 50% người dùng sử dụng bản v1.0.0
- 50% người dùng đã cập nhật lên v1.1.0
- Và Server bắt buộc phải hỗ trợ cả hai phiên bản này cùng lúc.
Nếu là Web thì sao? Chỉ với một lần deploy, tất cả người dùng sẽ ngay lập tức sử dụng code mới.
iOS có quy trình review, Android phát hành theo giai đoạn (Staged rollout)
Mọi thứ còn phức tạp hơn thế:
- iOS: Quá trình review app của Apple mất khoảng 2-3 ngày.
- Android: Việc phát hành theo giai đoạn (staged rollout) có thể mất vài ngày để đạt mức độ phủ sóng 100%.
- Cả hai nền tảng: Người dùng hoàn toàn có thể vô hiệu hóa tính năng tự động cập nhật.
Hệ quả là, tại bất kỳ thời điểm nào, sẽ luôn có ít nhất 3~4 phiên bản app đang được sử dụng song song.
Câu chuyện thực tế: Hậu quả khi App phụ thuộc trực tiếp vào DTO
Đọc bài viết chia sẻ kinh nghiệm của một Android developer, khi anh ấy nâng cấp API từ v3.5 lên v3.6:
- Cấu trúc response của endpoint
/usersbị thay đổi. - Kéo theo 2 endpoint khác cũng bị thay đổi chung.
- Kết quả: App bị crash ở những chỗ hoàn toàn không lường trước được.
Bạn có thể thắc mắc: "Chẳng phải lỗi do dev code ẩu sao?". Nhưng đứng ở góc độ của Mobile dev, họ thực sự không có sự lựa chọn. Bởi vì nếu nâng cấp phiên bản API toàn cục (Global API version), những thay đổi không lường trước được sẽ bị áp dụng đồng loạt lên toàn bộ hệ thống.
Giải pháp cuối cùng vẫn là Layer chuyển đổi (Transformation layer)
Trong những tình huống như thế này, nếu chúng ta có một layer chuyển đổi:
Ở bản App v1.0.0 (Lúc API còn trả về name):

Ở bản App v3.6 (Lúc API đã đổi thành firstName và lastName)

Tất cả các phiên bản của App sẽ sử dụng chung một Domain type duy nhất, và ta chỉ cần chuyển đổi (transform) response từ server cho phù hợp với từng phiên bản API là xong.
Trên Web, việc này có thể là tùy chọn (optional). Nhưng trên App, việc phụ thuộc trực tiếp vào DTO chẳng khác nào ôm một quả bom nổ chậm. Quản lý phiên bản API (API versioning) và xây dựng Layer chuyển đổi (Transformation layer) gần như là yêu cầu bắt buộc.
Nhưng liệu điều này có thực sự là vấn đề ở MỌI dự án không?
Sự thật là không. Theo kinh nghiệm của tôi, nó còn tùy thuộc vào hoàn cảnh của từng dự án.
Khi làm việc trong một team nhỏ, dùng trực tiếp DTO vẫn hoàn toàn ổn. Bạn ngồi cùng phòng với dev Backend, có thể bàn bạc trước mỗi khi API thay đổi, chia sẻ thông tin ngay lập tức và cùng nhau xử lý. Nếu số lượng API ít, số lượng component sử dụng cũng chẳng bao nhiêu, thì thà giữ mọi thứ đơn giản (KISS) lại càng tốt. Lúc này, áp dụng Transformation Layer có khi lại thành over-engineering (làm phức tạp hóa vấn đề không cần thiết).
Tuy nhiên, có những case mà tôi cực kỳ khuyên bạn nên dùng Transformation Layer.
Đó là khi làm dự án quy mô lớn và phải phụ thuộc vào nhiều API.
Chẳng hạn như khi bạn phải gọi tới hơn 10 API endpoint khác nhau, dùng các API ngoài tầm kiểm soát như API của bên thứ 3 (external API) hay đối tác, hoặc khi team Backend bị chia nhỏ thành nhiều team khiến bạn khó mà nắm bắt được toàn bộ thay đổi. Trong những tình huống này, sự thay đổi của một API bất kỳ có thể tạo ra "hiệu ứng domino" gây lỗi cho toàn bộ Frontend.
Đặc biệt, bạn đã bao giờ gặp cảnh này chưa?

Cùng là "Tên người dùng" nhưng mỗi API lại đặt một tên field khác nhau... chuyện này xảy ra như cơm bữa. Những lúc thế này, nếu cứ nhét vào từng component để rào đón cho khớp thì code sẽ cực kỳ lộn xộn.

Ngay cả Maintainer của React Query cũng khuyên dùng Data Transformation
Nếu bạn đọc bài blog React Query Data Transformations của TkDodo, bạn sẽ bắt gặp đoạn này:
"Let's face it - most of us are not using GraphQL. If you are working with REST though, you are constrained by what the backend returns."
Ý của câu này là: Nếu bạn đang sử dụng REST API, bạn sẽ bị ràng buộc bởi cấu trúc dữ liệu mà backend trả về (không linh hoạt như GraphQL). Chính vì vậy, TkDodo đã đề xuất một vài phương pháp để chuyển đổi dữ liệu (data transformation), và cách được ông ấy khuyên dùng nhất chính là sử dụng option select.

Ưu điểm của phương pháp này là: nếu dữ liệu sau khi transform không có sự thay đổi, component sẽ không bị re-render. Hơn nữa, bạn có thể subscribe một cách có chọn lọc chỉ những phần dữ liệu cần thiết từ toàn bộ payload ban đầu. Và một điểm cộng cực lớn nữa là bài toán chuyển đổi type (type transformation) được giải quyết dứt điểm ngay tại tầng useQuery.
Pattern tham khảo
Vậy thực tế chúng ta nên thiết kế kiến trúc như thế nào? Để tôi cho các bạn xem một pattern mà các bạn có thể tham khảo nhé.
Đầu tiên, tôi sẽ cấu trúc tầng Repository (Repository layer)


Và bên trong Custom Hook, tôi tận dụng option select mà TkDodo đã gợi ý.

Lúc này, Component sẽ chỉ sử dụng Domain type.

Giờ giả sử Backend quyết định thay đổi API, gộp firstName và lastName thành fullName thì sao?

Component không cần đụng tới. Lỗi type cũng hoàn toàn không xảy ra.
Ưu điểm của pattern này là:
- Logic chuyển đổi (transformation logic) được tập trung tại
formatters.tsnên rất dễ quản lý. - Tận dụng trọn vẹn cơ chế tối ưu hóa (optimization) của option
selecttrong React Query. - Việc gọi API và logic chuyển đổi dữ liệu được tách biệt cực kỳ rõ ràng.
Ưu điểm thực sự của cách tiếp cận này: Khoanh vùng điểm phát sinh lỗi
Nếu đặt một layer chuyển đổi (transformation layer), việc xác định vấn đề xảy ra ở đâu sẽ trở nên cực kỳ rõ ràng.
Nếu dùng trực tiếp DTO, lỗi type sẽ nổ ra ở 15 component khác nhau, và bạn sẽ rất khó để biết rốt cuộc là sai ở đâu. Nhưng nếu dùng layer chuyển đổi, lỗi type chỉ xuất hiện duy nhất ở formatter, bạn chỉ cần nhìn vào một chỗ và sửa ở đúng một chỗ đó.

Đặc biệt trong các dự án quy mô lớn sử dụng nhiều API, sự khác biệt này càng rõ rệt. Giả sử dự án có 10 API:
- Khi dùng trực tiếp DTO: 10 API × trung bình 8 component = Lỗi ở 80 chỗ
- Khi dùng formatter: Chỉ báo lỗi ở 10 file formatter
Số lượng vị trí phát sinh lỗi giảm từ 80 xuống còn 10. Và số chỗ bạn cần sửa cũng chỉ còn 10.
Tất nhiên là phải có sự đánh đổi (Trade-off)
"Vậy chẳng phải boilerplate code sẽ phình ra rất nhiều sao?"
Đúng vậy. Bạn sẽ phải viết các hàm chuyển đổi, định nghĩa các Domain type và phải quản lý thêm các file mới. Đó chính là chi phí (cost) phải bỏ ra.
Tuy nhiên, lợi ích thu lại là sự cô lập (isolation) khỏi những thay đổi từ server, tính ổn định của component, type safety (an toàn kiểu dữ liệu) và giảm thiểu chi phí bảo trì trong dài hạn.
Vì vậy, theo tôi:
- Dưới 3 API + giao tiếp sát sao với Backend → Dùng trực tiếp DTO vẫn ổn.
- Môi trường API ổn định, ít thay đổi → Chỉ transform cục bộ những chỗ thực sự cần.
- Dự án quy mô vừa + đang ở giai đoạn dev năng động → Khuyên dùng Transformation Layer toàn cục.
- Hơn 10 API + phụ thuộc nhiều team → Cực kỳ khuyên dùng Transformation Layer (gần như bắt buộc).
Muốn áp dụng thì bắt đầu từ đâu?
Nếu đọc đến đây và cảm thấy đồng tình, bạn có thể bắt đầu theo cách sau.
Trước tiên, hãy định nghĩa Domain type.

Sau đó, thêm formatter vào tầng Repository.

Cuối cùng, tiến hành migration dần dần cho các code hiện tại.

Khi type bị thay đổi, chắc chắn lỗi type sẽ bắn ra ở các component. Lúc đó bạn chỉ cần đi sửa từng component một là được. Đây chính là sức mạnh của việc phát hiện vấn đề ngay từ lúc compile time.
Lời kết
"Dự án của bạn đang dùng bao nhiêu API, và bạn đang giao tiếp sát sao với dev Backend đến mức nào?"
Câu trả lời cho câu hỏi này rốt cuộc sẽ là kim chỉ nam để bạn quyết định xem nên dùng trực tiếp DTO hay xây dựng một layer chuyển đổi.
Lý do tôi viết bài này là vì tôi tin rằng không chỉ mình tôi, mà chắc hẳn nhiều anh em Frontend dev khác cũng đang có chung nỗi niềm trăn trở này. Sẽ không có một đáp án nào là hoàn hảo tuyệt đối cho mọi tình huống, nhưng hy vọng bài viết này sẽ giúp ích phần nào cho quyết định của các bạn dựa trên hoàn cảnh thực tế của dự án.
Các bạn cũng nên tìm đọc thử blog của TkDodo nhé. Thực sự có rất nhiều insight hay ho ở đó.
Ngoài ra nếu muốn đào sâu hơn nữa, học các kiến thức chuyên sâu thực tiễn hơn từ giảng viên là chuyên gia có hơn 10 năm kinh nghiệm frontend và từng là kỹ sư tại Shopee Singapo thì bạn có thể tham khảo khoá học React Nâng Cao bên dưới của Sydexa nha!
