Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Request body formats: JSON, form-urlencoded, multipart

API and Technical Fundamentals cho BA Bài 11/60

Mở đầu — vì sao bài này quan trọng

Khi một hệ thống cần gửi dữ liệu lên server — tạo đơn hàng, đăng ký tài khoản, upload chứng từ — dữ liệu đó nằm trong phần gọi là request body (thân của yêu cầu). Ở các bài trước bạn đã học cấu trúc HTTP request/response và các method như POST, PUT, PATCH. Bài này trả lời một câu hỏi cụ thể hơn nhưng cực kỳ thực tế: dữ liệu trong body được đóng gói theo định dạng nào?

Đây không phải chuyện "kỹ thuật của dev". Là một BA (Business Analyst), bạn sẽ liên tục chạm vào nó: khi viết API spec, khi đọc tài liệu tích hợp của đối tác (ví dụ cổng thanh toán VNPAY, MoMo), khi ngồi cùng QA debug một lỗi "form không submit được", hay khi giải thích cho khách hàng vì sao tính năng upload hóa đơn lại "khó hơn tưởng tượng". Ba định dạng body phổ biến nhất — application/json, application/x-www-form-urlencoded, và multipart/form-data — mỗi loại sinh ra để giải quyết một bài toán khác nhau. Chọn sai định dạng là nguyên nhân của vô số lỗi tích hợp mà nhìn bề ngoài rất khó hiểu: "code đúng hết mà server trả 400".

Hiểu rõ ba định dạng này giúp bạn viết yêu cầu rõ ràng, đọc tài liệu API không bị "ngợp", và quan trọng nhất là phán đoán đúng nguyên nhân khi tích hợp trục trặc — kỹ năng phân biệt một BA technical với một BA chỉ biết viết user story.

Khái niệm cốt lõi

Trước khi đi vào từng định dạng, cần nắm một nguyên tắc nền: header Content-Type quyết định cách server đọc body. Body chỉ là một chuỗi byte. Chính Content-Type nói cho server biết "hãy diễn giải đống byte này theo luật nào". Gửi nội dung JSON nhưng khai báo Content-Type sai là lỗi kinh điển — server sẽ cố đọc theo luật khác và báo lỗi.

1. application/json — định dạng chủ đạo của API hiện đại

Đây là định dạng bạn gặp 80% thời gian khi làm việc với REST API ngày nay. Body là một chuỗi JSON thuần.

POST /api/orders HTTP/1.1
Host: api.shop.vn
Content-Type: application/json

{ "customer_id": 123, "shipping_address": "12 Nguyễn Huệ, Q1, TP.HCM", "items": [ { "sku": "AO-THUN-XANH-M", "quantity": 2, "price": 199000 }, { "sku": "QUAN-JEAN-32", "quantity": 1, "price": 459000 } ], "voucher": null }

Vì sao JSON thống trị? Vì nó diễn đạt được dữ liệu phức tạp một cách tự nhiên: object lồng nhau, mảng (array), kiểu dữ liệu rõ ràng (số, chuỗi, boolean, null). Một đơn hàng có nhiều sản phẩm, mỗi sản phẩm lại có nhiều thuộc tính — JSON thể hiện cấu trúc lồng đó dễ dàng. (Chi tiết về các kiểu dữ liệu, nested, array sẽ được đào sâu ở Bài 12; ở đây ta chỉ quan tâm nó nằm trong body như thế nào.)

Điểm cần nhớ với tư cách BA: JSON gửi được cấu trúc cây sâu, nhưng không gửi trực tiếp được file nhị phân (ảnh, PDF). Nếu cần đính kèm file trong JSON, người ta phải mã hóa file thành chuỗi Base64 — làm dung lượng phình thêm khoảng 33% và thường không khuyến khích cho file lớn.

2. application/x-www-form-urlencoded — định dạng của form truyền thống

Đây là định dạng mặc định khi bạn submit một thẻ <form> HTML cơ bản (không có upload file). Dữ liệu được nối thành chuỗi dạng key=value, ngăn cách bằng dấu &, giống hệt phần query string trên URL.

POST /login HTTP/1.1
Host: portal.congty.vn
Content-Type: application/x-www-form-urlencoded

username=nguyenvana&password=MatKhau%40123&remember=true

Lưu ý ký tự đặc biệt phải được URL-encode: dấu @ thành %40, dấu cách thành %20 hoặc +. Định dạng này phẳng (flat) — chỉ là danh sách cặp key–value, không diễn đạt tốt cấu trúc lồng nhau như JSON. Muốn gửi một mảng, người ta phải dùng quy ước (ví dụ items[0][sku]=...) rất lủng củng.

Khi nào còn gặp nó? Các hệ thống cũ, một số cổng thanh toán và SSO, các endpoint OAuth token (chuẩn OAuth 2.0 quy định body của bước đổi token dùng đúng x-www-form-urlencoded — bạn sẽ thấy lại ở Bài 17), và các form web đơn giản.

3. multipart/form-data — định dạng để gửi file

Khi cần upload file (ảnh, PDF, Excel) — đặc biệt khi gửi kèm các trường text khác — đây là định dạng bắt buộc. Body được chia thành nhiều "phần" (part), mỗi part có header riêng, ngăn cách bằng một chuỗi gọi là boundary.

POST /api/products HTTP/1.1
Host: api.shop.vn
Content-Type: multipart/form-data; boundary=----Boundary7MA4

------Boundary7MA4 Content-Disposition: form-data; name="name"

Áo khoác mùa đông ------Boundary7MA4 Content-Disposition: form-data; name="price"

890000 ------Boundary7MA4 Content-Disposition: form-data; name="image"; filename="ao-khoac.jpg" Content-Type: image/jpeg

<dữ liệu nhị phân của file ảnh> ------Boundary7MA4--

Mỗi part tự mô tả mình qua Content-Disposition (tên trường) và có thể có Content-Type riêng (ví dụ image/jpeg cho file ảnh). Nhờ vậy một request có thể vừa mang text vừa mang file nhị phân — điều mà JSON và form-urlencoded không làm gọn được. Đổi lại, multipart "nặng" hơn về cấu trúc và bạn gần như không bao giờ tự gõ tay nó; công cụ (Postman, thư viện code, trình duyệt) tự sinh boundary.

So sánh nhanh ba định dạng

Tiêu chíapplication/jsonx-www-form-urlencodedmultipart/form-data
Cấu trúc lồng nhauTốt nhấtKém (phải dùng quy ước)Trung bình
Gửi file nhị phânKhông (phải Base64)KhôngCó — đúng mục đích
Dễ đọc bằng mắtCaoTrung bìnhThấp
Trường hợp dùngREST API hiện đạiForm cũ, OAuth token, SSOUpload file + text

Tình huống thực tế

Ví dụ 1 — Tiki: lỗi "tạo đơn thất bại" chỉ vì khai báo Content-Type sai

Một đội tích hợp của đối tác bán hàng kết nối vào API tạo đơn của một sàn TMĐT (giả định theo bối cảnh Tiki). Họ viết code gửi body JSON đúng chuẩn, nhưng để nguyên Content-Type: application/x-www-form-urlencoded (copy nhầm từ một ví dụ login cũ). Server nhận được chuỗi {"customer_id":123,...} nhưng được lệnh đọc theo luật form-urlencoded, nên nó hiểu toàn bộ là một cái key khổng lồ không có value và trả về 400 Bad Request: customer_id is required.

Diễn giải: Đội dev mất nửa ngày soi vì "rõ ràng có gửi customer_id mà". BA trong nhóm — vốn hiểu nguyên lý Content-Type quyết định cách đọc body — chỉ liếc qua header là chỉ ra ngay điểm sai.

Bài học: Khi gặp lỗi "trường bắt buộc bị thiếu" dù dữ liệu trông như có đủ, việc đầu tiên cần kiểm tra là Content-Type có khớp với định dạng body thật hay không. Đây là lỗi tích hợp số một mà BA nên thuộc lòng.

Ví dụ 2 — Cổng thanh toán VNPAY/MoMo: vì sao tài liệu bắt dùng form-urlencoded

Một startup fintech ở TP.HCM tích hợp cổng thanh toán nội địa. Đội ngũ quen làm REST API JSON nên mặc định gửi request hoàn tiền (refund) bằng application/json. Nhưng tài liệu của cổng quy định endpoint này nhận application/x-www-form-urlencoded, với các tham số phẳng như vnp_TxnRef, vnp_Amount, vnp_SecureHash. Kết quả: chữ ký bảo mật (secure hash) tính ra không khớp, mọi giao dịch refund bị từ chối.

Diễn giải: Các cổng thanh toán VN và nhiều hệ thống ngân hàng được xây từ lâu, theo chuẩn form-urlencoded, và phần ký số được tính trên chuỗi đã sắp xếp dạng key=value&key=value. Đổi sang JSON là phá vỡ luôn cách tính chữ ký.

Bài học: BA phụ trách tích hợp thanh toán phải đọc kỹ mục "Content-Type" và "định dạng tham số" trong tài liệu đối tác, đưa nó vào API spec rõ ràng, và cảnh báo đội dev đừng "mặc định JSON". Một dòng ghi chú trong spec ở đây tiết kiệm hàng tuần debug.

Ví dụ 3 — Sàn TMĐT nội bộ: nghiệp vụ "đăng sản phẩm kèm ảnh" buộc dùng multipart

Một BA tại doanh nghiệp bán lẻ (giả định: chuỗi mỹ phẩm 50 cửa hàng) viết spec cho tính năng nhân viên đăng sản phẩm mới: nhập tên, giá, mô tả, và up tối đa 5 ảnh sản phẩm. Đội dev ban đầu định gửi tất cả bằng JSON và encode ảnh thành Base64. BA chỉ ra hai vấn đề: (1) năm ảnh chụp điện thoại ~3 MB mỗi tấm, sau Base64 phình lên ~4 MB, tổng body gần 20 MB — chậm và dễ chạm giới hạn kích thước request; (2) khó đặt giới hạn dung lượng từng file vì tất cả trộn vào một chuỗi.

Giải pháp: dùng multipart/form-data, mỗi ảnh là một part riêng có Content-Type: image/jpeg, các trường text (tên, giá) là các part text. Server kiểm tra dung lượng từng part dễ dàng và áp giới hạn 5 MB/ảnh.

Bài học: Hễ nghiệp vụ có "upload file kèm thông tin khác", BA nên chủ động ghi rõ trong spec là dùng multipart/form-data, kèm ràng buộc: số file tối đa, dung lượng mỗi file, định dạng cho phép. Điều này khớp với chính trải nghiệm sản phẩm thực tế — như tính năng cho phép đính kèm PDF/PPT vào bài post cộng đồng, mỗi file tối đa 10 MB, tối đa 3 file: đó chính là một spec multipart được mô tả bằng ngôn ngữ nghiệp vụ.

Hướng dẫn từng bước

Cách một BA chọn và mô tả định dạng body khi viết hoặc review API spec:

  • Xác định bản chất dữ liệu cần gửi. Hỏi: dữ liệu có file nhị phân không? Có cấu trúc lồng nhau (object trong object, mảng các object) không?
  • Áp quy tắc chọn định dạng:
- Có file (kèm hoặc không kèm text) → multipart/form-data. - Không file, dữ liệu có cấu trúc/lồng nhau → application/json (mặc định nên chọn cho API mới). - Không file, dữ liệu phẳng, đối tác/hệ thống cũ yêu cầu → application/x-www-form-urlencoded.

  • Ghi rõ Content-Type trong spec. Đừng để ngầm hiểu. Một dòng "Request Content-Type: application/json" trong tài liệu API tránh được vô số tranh cãi.
  • Mô tả cấu trúc body kèm ví dụ mẫu. Với JSON: đưa một payload ví dụ đầy đủ. Với multipart: liệt kê tên từng part (text part nào, file part nào), kiểu file, ràng buộc dung lượng/số lượng.
  • Đối chiếu với response và mã lỗi. Ghi rõ server trả gì khi body sai định dạng (thường là 400 hoặc 415 Unsupported Media Type) — giúp QA viết test case và dev xử lý lỗi nhất quán.
  • Kiểm chứng bằng công cụ. Trước khi chốt spec, tự gửi thử bằng Postman (Bài 14) hoặc curl (Bài 15): chọn đúng mode body, xem request thực tế khớp với mô tả không. Một BA technical luôn "chạy thử" spec của mình.

Lỗi thường gặp & mẹo

  • Content-Type không khớp body. Lỗi phổ biến nhất. Gửi JSON nhưng khai báo form-urlencoded, hoặc ngược lại. Triệu chứng điển hình: "trường bắt buộc bị thiếu" dù dữ liệu có vẻ đầy đủ. Mẹo: khi debug, kiểm tra header Content-Type trước, payload sau.
  • Tự đặt Content-Type cho multipart và quên boundary. Boundary phải do công cụ tự sinh và đính kèm trong header. Nếu code tự hard-code Content-Type: multipart/form-data mà thiếu phần ; boundary=..., server không tách được các part. Mẹo: để thư viện/Postman tự sinh header này, đừng ghi đè tay.
  • Nhồi file vào JSON bằng Base64 cho file lớn. Hoạt động được với file nhỏ (logo, avatar), nhưng tốn ~33% dung lượng và dễ vượt giới hạn request với file lớn. Mẹo: file lớn → multipart; file rất nhỏ và bắt buộc nằm trong JSON → cân nhắc Base64 nhưng nêu rõ giới hạn.
  • Quên URL-encode trong form-urlencoded. Ký tự @, &, dấu cách, tiếng Việt có dấu phải được mã hóa. Quên là dữ liệu bị cắt hoặc sai. Mẹo: công cụ thường tự encode, nhưng khi ghép chuỗi thủ công thì phải tự encode.
  • Mặc định "API nào cũng JSON". Đặc biệt nguy hiểm khi tích hợp cổng thanh toán, SSO, hệ thống ngân hàng VN vốn theo chuẩn form-urlencoded. Mẹo: luôn đọc kỹ tài liệu đối tác, không suy diễn theo thói quen.
  • Mẹo BA "vàng": Trong mọi API spec, thêm một dòng cố định "Content-Type của request" cạnh tên endpoint. Nhỏ nhưng loại bỏ phần lớn hiểu lầm giữa team backend, frontend, và đối tác.

Bài tập thực hành

  • Phân loại định dạng. Với mỗi nghiệp vụ sau, chọn định dạng body phù hợp và giải thích một câu vì sao:
(a) Đăng ký tài khoản gồm email, mật khẩu, ngày sinh. (b) Nhân viên upload bản scan CMND (1 file ảnh) kèm họ tên. (c) Đổi token trong luồng OAuth 2.0 với một cổng SSO. (d) Tạo đơn hàng có 3 sản phẩm, mỗi sản phẩm có SKU, số lượng, giá.

  • Soi lỗi. Một dev báo "API trả 400, customer_id is required" nhưng khẳng định đã gửi customer_id. Hãy liệt kê 2 nguyên nhân liên quan đến định dạng body bạn sẽ kiểm tra đầu tiên, và cách kiểm tra.
  • Viết một mẩu spec. Mô tả body cho endpoint POST /api/invoices cho phép tạo hóa đơn gồm: mã khách hàng, danh sách dòng hàng (mỗi dòng có mô tả + thành tiền), và đính kèm 1 file PDF hóa đơn gốc. Ghi rõ: Content-Type, các trường, ràng buộc file (định dạng, dung lượng tối đa). Gợi ý: vì có file kèm dữ liệu lồng nhau, hãy cân nhắc cách biểu diễn danh sách dòng hàng trong multipart.
  • Thực hành công cụ (nâng cao). Mở Postman, tạo cùng một request POST tới một endpoint test (ví dụ https://httpbin.org/post) ba lần với ba mode body khác nhau (raw JSON, x-www-form-urlencoded, form-data). Quan sát phần headers và cách httpbin phản chiếu dữ liệu để thấy rõ sự khác biệt.

Tóm tắt

  • Request body là nơi dữ liệu được gửi lên server, và Content-Type quyết định server đọc body theo luật nào — sai header này là nguồn lỗi tích hợp số một.
  • application/json: chủ đạo cho REST API hiện đại, mạnh ở dữ liệu có cấu trúc lồng nhau, nhưng không gửi trực tiếp file nhị phân.
  • application/x-www-form-urlencoded: dữ liệu phẳng dạng key=value&..., còn dùng nhiều ở form cũ, OAuth token, SSO, và cổng thanh toán VN.
  • multipart/form-data: bắt buộc khi upload file (đặc biệt file kèm text), chia body thành các part ngăn cách bởi boundary, mỗi part có thể có Content-Type riêng.
  • Với BA: chọn định dạng theo bản chất dữ liệu (có file? có lồng nhau?), ghi rõ Content-Type và ví dụ payload trong spec, lường trước mã lỗi 400/415, và luôn chạy thử bằng Postman/curl trước khi chốt. Đừng bao giờ mặc định "API nào cũng JSON" — nhất là khi làm việc với hệ thống ngân hàng và thanh toán nội địa.