← Trở về trang chủ

Request Body

Định nghĩa

Request Body là phần dữ liệu chính được gửi trong HTTP request, chứa payload (tải trọng) mà client muốn gửi đến server.

Cấu trúc HTTP Request với Body

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 58
Authorization: Bearer token123

{
  "name": "Nguyễn Văn A",
  "email": "vana@example.com"
}

Phân tích:

  • Request Line: POST /api/users HTTP/1.1
  • Headers: Host, Content-Type, Content-Length, Authorization
  • Blank Line: Dòng trống ngăn cách headers và body
  • Body: JSON data với name và email

Khi nào Request có Body?

HTTP Method Có Body? Mục đích
POST ✓ Yes Tạo mới resource
PUT ✓ Yes Cập nhật toàn bộ resource
PATCH ✓ Yes Cập nhật một phần resource
DELETE Optional Xóa resource (có thể có metadata)
GET ✗ No Lấy dữ liệu (dùng query params)
HEAD ✗ No Lấy headers only
OPTIONS ✗ No Kiểm tra methods được hỗ trợ
⚠️ Lưu ý về GET requests:

Mặc dù HTTP spec không cấm GET có body, nhưng:

  • Nhiều web servers/frameworks ignore GET request body
  • Caching không work đúng với GET + body
  • HTTP spec khuyến nghị KHÔNG dùng body với GET
  • Nếu cần gửi complex data cho search/filter, dùng POST thay vì GET

Body chứa gì?

  • Data để create/update: User info, product details, order data
  • Complex search criteria: Advanced filters không fit trong query params
  • File uploads: Images, documents, videos
  • Bulk operations: Multiple records cùng lúc
  • Structured data: Nested objects, arrays
  • Sensitive information: Passwords, credit cards (phải dùng HTTPS)

Ví dụ cơ bản với Playwright

import { test, expect } from '@playwright/test';

test('POST với JSON body', async ({ request }) => {
  const response = await request.post('https://api.example.com/users', {
    data: {
      name: 'Nguyễn Văn A',
      email: 'vana@example.com',
      age: 25,
      city: 'Hà Nội'
    }
  });

  expect(response.status()).toBe(201);

  const user = await response.json();
  expect(user).toHaveProperty('id');
  expect(user.name).toBe('Nguyễn Văn A');
});

test('PUT để cập nhật user', async ({ request }) => {
  const response = await request.put('https://api.example.com/users/123', {
    data: {
      name: 'Nguyễn Văn A - Updated',
      email: 'vana_new@example.com',
      age: 26,
      city: 'TP.HCM'
    }
  });

  expect(response.status()).toBe(200);
});

test('PATCH để cập nhật một phần', async ({ request }) => {
  const response = await request.patch('https://api.example.com/users/123', {
    data: {
      city: 'Đà Nẵng'  // Chỉ update city
    }
  });

  expect(response.status()).toBe(200);
});

application/json - Phổ biến nhất cho REST APIs

Đặc điểm:

  • ✓ Dễ đọc và parse
  • ✓ Support nested objects và arrays
  • ✓ Language-independent
  • ✓ Lightweight
  • ✓ Wide support (JavaScript, PHP, Python, ...)

Simple Object

POST /api/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securePassword123"
}

Nested Object

POST /api/orders
Content-Type: application/json

{
  "customer": {
    "name": "Nguyễn Văn A",
    "email": "vana@example.com",
    "phone": "0123456789"
  },
  "items": [
    {
      "product_id": 101,
      "quantity": 2,
      "price": 500000
    },
    {
      "product_id": 202,
      "quantity": 1,
      "price": 1000000
    }
  ],
  "shipping_address": {
    "street": "123 Main St",
    "city": "Hà Nội",
    "district": "Ba Đình",
    "country": "Vietnam"
  },
  "payment_method": "credit_card",
  "notes": "Giao hàng giờ hành chính"
}

Playwright - JSON Body

import { test, expect } from '@playwright/test';

test('POST JSON - Simple', async ({ request }) => {
  const response = await request.post('https://api.example.com/login', {
    data: {
      email: 'user@example.com',
      password: 'password123'
    }
    // Playwright tự động set Content-Type: application/json
  });

  expect(response.status()).toBe(200);

  const result = await response.json();
  expect(result).toHaveProperty('token');
});

test('POST JSON - Nested Objects', async ({ request }) => {
  const response = await request.post('https://api.example.com/orders', {
    data: {
      customer: {
        name: 'Nguyễn Văn A',
        email: 'vana@example.com',
        phone: '0123456789'
      },
      items: [
        { product_id: 101, quantity: 2, price: 500000 },
        { product_id: 202, quantity: 1, price: 1000000 }
      ],
      shipping_address: {
        street: '123 Main St',
        city: 'Hà Nội',
        country: 'Vietnam'
      },
      payment_method: 'credit_card'
    }
  });

  expect(response.status()).toBe(201);

  const order = await response.json();
  expect(order).toHaveProperty('id');
  expect(order).toHaveProperty('total');
  expect(order.items).toHaveLength(2);
});

PHP - Đọc JSON Body

<?php
// Đọc JSON từ request body
$json = file_get_contents('php://input');
$data = json_decode($json, true);

// Validate
if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Access data
$email = $data['email'] ?? null;
$password = $data['password'] ?? null;

if (!$email || !$password) {
    http_response_code(400);
    echo json_encode(['error' => 'Email and password required']);
    exit;
}

// Process login...
$result = ['token' => 'abc123xyz', 'user_id' => 1];

// Send JSON response
header('Content-Type: application/json');
echo json_encode($result);
?>

JSON Data Types

{
  "string": "Hello World",
  "number": 123,
  "float": 12.34,
  "boolean": true,
  "null_value": null,
  "array": [1, 2, 3, "four"],
  "object": {
    "nested": "value"
  }
}
⚠️ JSON Best Practices:
  • Always validate JSON structure
  • Set correct Content-Type: application/json
  • Use consistent naming (camelCase or snake_case)
  • Avoid circular references
  • Don't send undefined values (omit field hoặc dùng null)

application/x-www-form-urlencoded - HTML Form Default

Đặc điểm:

  • ✓ Default cho HTML forms
  • ✓ Simple key-value pairs
  • ✓ URL encoded format
  • ✗ Không support nested objects
  • ✗ Không support file uploads

Format

POST /api/login
Content-Type: application/x-www-form-urlencoded

email=user%40example.com&password=password123&remember=true

Giống query string nhưng ở trong body!

Playwright - Form Urlencoded

import { test, expect } from '@playwright/test';

test('POST form-urlencoded', async ({ request }) => {
  const response = await request.post('https://api.example.com/login', {
    form: {  // ← Dùng 'form' thay vì 'data'
      email: 'user@example.com',
      password: 'password123',
      remember: 'true'
    }
  });

  expect(response.status()).toBe(200);
});

// Hoặc set header manually
test('POST form-urlencoded (manual)', async ({ request }) => {
  const params = new URLSearchParams({
    email: 'user@example.com',
    password: 'password123',
    remember: 'true'
  });

  const response = await request.post('https://api.example.com/login', {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: params.toString()
  });

  expect(response.status()).toBe(200);
});

PHP - Đọc Form Urlencoded

<?php
// PHP tự động parse form-urlencoded vào $_POST
$email = $_POST['email'] ?? null;
$password = $_POST['password'] ?? null;
$remember = $_POST['remember'] ?? false;

// Hoặc đọc raw
$body = file_get_contents('php://input');
parse_str($body, $data);

$email = $data['email'] ?? null;
$password = $data['password'] ?? null;
?>

URL Encoding Rules

Character Encoded
Space + or %20
@ %40
& %26
= %3D
% %25
⚠️ Khi nào dùng form-urlencoded?
  • ✓ Simple forms với flat key-value data
  • ✓ Legacy systems expecting this format
  • ✓ OAuth/authentication flows
  • ✗ KHÔNG dùng cho nested objects (dùng JSON)
  • ✗ KHÔNG dùng cho file uploads (dùng multipart)

multipart/form-data - File Uploads

Đặc điểm:

  • ✓ Dùng cho file uploads
  • ✓ Mixed content types (text + binary)
  • ✓ Multiple files cùng lúc
  • ✓ Form fields + files
  • ✗ Phức tạp hơn JSON

Format

POST /api/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="title"

My Document
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"

Document description here
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="document.pdf"
Content-Type: application/pdf

[binary PDF data here]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

Playwright - File Upload

import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';

test('Upload file with form data', async ({ request }) => {
  // Đọc file
  const filePath = path.join(__dirname, 'files', 'document.pdf');
  const fileBuffer = fs.readFileSync(filePath);

  const response = await request.post('https://api.example.com/upload', {
    multipart: {
      title: 'My Document',
      description: 'Important document',
      category: 'reports',
      file: {
        name: 'document.pdf',
        mimeType: 'application/pdf',
        buffer: fileBuffer
      }
    }
  });

  expect(response.status()).toBe(201);

  const result = await response.json();
  expect(result).toHaveProperty('file_id');
  expect(result).toHaveProperty('url');
});

test('Upload multiple files', async ({ request }) => {
  const file1 = fs.readFileSync('image1.jpg');
  const file2 = fs.readFileSync('image2.jpg');

  const response = await request.post('https://api.example.com/upload-multiple', {
    multipart: {
      title: 'Photo Album',
      files: [
        {
          name: 'image1.jpg',
          mimeType: 'image/jpeg',
          buffer: file1
        },
        {
          name: 'image2.jpg',
          mimeType: 'image/jpeg',
          buffer: file2
        }
      ]
    }
  });

  expect(response.status()).toBe(201);
});

test('Upload image from URL', async ({ request }) => {
  // Fetch image từ URL
  const imageResponse = await request.get('https://via.placeholder.com/150');
  const imageBuffer = await imageResponse.body();

  const uploadResponse = await request.post('https://api.example.com/upload', {
    multipart: {
      name: 'avatar',
      file: {
        name: 'avatar.png',
        mimeType: 'image/png',
        buffer: imageBuffer
      }
    }
  });

  expect(uploadResponse.status()).toBe(201);
});

PHP - Handle File Upload

<?php
// Check if file uploaded
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    echo json_encode(['error' => 'No file uploaded']);
    exit;
}

$file = $_FILES['file'];
$title = $_POST['title'] ?? 'Untitled';
$description = $_POST['description'] ?? '';

// Validate file
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($file['type'], $allowedTypes)) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid file type']);
    exit;
}

// Validate size (max 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
    http_response_code(400);
    echo json_encode(['error' => 'File too large']);
    exit;
}

// Generate unique filename
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFilename = uniqid() . '.' . $ext;
$uploadPath = '/var/www/uploads/' . $newFilename;

// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
    http_response_code(500);
    echo json_encode(['error' => 'Failed to save file']);
    exit;
}

// Save to database
$fileId = saveToDatabase($title, $description, $newFilename, $file['size']);

// Return response
header('Content-Type: application/json');
echo json_encode([
    'success' => true,
    'file_id' => $fileId,
    'url' => 'https://cdn.example.com/uploads/' . $newFilename,
    'size' => $file['size']
]);
?>
⚠️ File Upload Security:
  • ✓ Validate file type (MIME type + extension)
  • ✓ Validate file size (set maximum)
  • ✓ Scan for malware/viruses
  • ✓ Generate unique filename (không dùng original name)
  • ✓ Store uploads outside web root nếu có thể
  • ✓ Set proper permissions (không executable)
  • ✓ Use Content-Disposition header khi serve files

application/xml - Legacy & Enterprise

Đặc điểm:

  • ✓ Structured data
  • ✓ Schema validation (XSD)
  • ✓ Namespaces support
  • ✗ Verbose (dài dòng hơn JSON)
  • ✗ Ít phổ biến cho REST APIs hiện đại

XML Format

POST /api/users
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<user>
  <name>Nguyễn Văn A</name>
  <email>vana@example.com</email>
  <age>25</age>
  <address>
    <city>Hà Nội</city>
    <country>Vietnam</country>
  </address>
</user>

Playwright - XML Body

import { test, expect } from '@playwright/test';

test('POST XML data', async ({ request }) => {
  const xmlData = `<?xml version="1.0" encoding="UTF-8"?>
<user>
  <name>Nguyễn Văn A</name>
  <email>vana@example.com</email>
  <age>25</age>
</user>`;

  const response = await request.post('https://api.example.com/users', {
    headers: {
      'Content-Type': 'application/xml'
    },
    data: xmlData
  });

  expect(response.status()).toBe(201);

  // Parse XML response
  const responseText = await response.text();
  expect(responseText).toContain('<user>');
});

PHP - Parse XML

<?php
$xml = file_get_contents('php://input');

// Parse XML
$doc = simplexml_load_string($xml);

if ($doc === false) {
    http_response_code(400);
    echo '<error>Invalid XML</error>';
    exit;
}

// Access data
$name = (string)$doc->name;
$email = (string)$doc->email;
$age = (int)$doc->age;

// Process...

// Send XML response
header('Content-Type: application/xml');
echo '<?xml version="1.0"?>';
echo '<response>';
echo '  <success>true</success>';
echo '  <user_id>123</user_id>';
echo '</response>';
?>

XML vs JSON

Feature XML JSON
Kích thước Lớn hơn Nhỏ hơn
Dễ đọc Trung bình Dễ
Parse speed Chậm hơn Nhanh hơn
Schema validation XSD JSON Schema
Namespaces Yes No
Comments Yes No
Use case Legacy, SOAP, Enterprise REST APIs, Web, Mobile

text/plain - Raw Text

POST /api/notes
Content-Type: text/plain

This is a simple text note.
Multiple lines supported.
No special formatting.

application/octet-stream - Binary Data

POST /api/upload
Content-Type: application/octet-stream

[raw binary data]

Playwright Examples

import { test, expect } from '@playwright/test';

// Text/plain
test('POST plain text', async ({ request }) => {
  const response = await request.post('https://api.example.com/notes', {
    headers: {
      'Content-Type': 'text/plain'
    },
    data: 'This is my note content.\nMultiple lines supported.'
  });

  expect(response.status()).toBe(201);
});

// Binary data
test('POST binary data', async ({ request }) => {
  const buffer = Buffer.from([0x89, 0x50, 0x4E, 0x47]); // PNG header

  const response = await request.post('https://api.example.com/upload', {
    headers: {
      'Content-Type': 'application/octet-stream'
    },
    data: buffer
  });

  expect(response.status()).toBe(201);
});

POST - Create Resource

Tạo mới một resource. Body chứa toàn bộ dữ liệu của resource mới.

POST /api/posts
Content-Type: application/json

{
  "title": "New Blog Post",
  "content": "This is the content of my new blog post...",
  "status": "draft",
  "tags": ["api", "testing", "playwright"],
  "publish_date": "2025-02-01"
}

// Response: 201 Created
{
  "id": 456,
  "title": "New Blog Post",
  "status": "draft",
  "created_at": "2025-01-15T10:00:00Z",
  "author_id": 123
}

PUT - Full Update

Cập nhật toàn bộ resource. Body phải chứa TẤT CẢ fields.

PUT /api/posts/456
Content-Type: application/json

{
  "title": "Updated Blog Post",
  "content": "Updated content here...",
  "status": "published",
  "tags": ["api", "testing"],
  "publish_date": "2025-01-20"
}

// ⚠️ Nếu thiếu field nào đó, field đó có thể bị xóa/reset!

PATCH - Partial Update

Cập nhật một phần resource. Body chỉ chứa fields cần update.

PATCH /api/posts/456
Content-Type: application/json

{
  "status": "published",
  "publish_date": "2025-01-20"
}

// Chỉ update status và publish_date
// Các fields khác giữ nguyên

DELETE - Optional Body

Xóa resource. Body là optional, có thể chứa metadata hoặc lý do xóa.

DELETE /api/posts/456
Content-Type: application/json

{
  "reason": "spam",
  "notify_author": false,
  "cascade": true
}

// Hoặc không có body
DELETE /api/posts/456

Test với Playwright

import { test, expect } from '@playwright/test';

test('Full CRUD workflow', async ({ request }) => {
  // CREATE - POST
  const createResponse = await request.post('https://api.example.com/posts', {
    data: {
      title: 'Test Post',
      content: 'Content here',
      status: 'draft'
    }
  });
  expect(createResponse.status()).toBe(201);
  const post = await createResponse.json();
  const postId = post.id;

  // READ - GET (không có body)
  const getResponse = await request.get(`https://api.example.com/posts/${postId}`);
  expect(getResponse.status()).toBe(200);

  // UPDATE FULL - PUT
  const putResponse = await request.put(`https://api.example.com/posts/${postId}`, {
    data: {
      title: 'Updated Post',
      content: 'Updated content',
      status: 'published'
    }
  });
  expect(putResponse.status()).toBe(200);

  // UPDATE PARTIAL - PATCH
  const patchResponse = await request.patch(`https://api.example.com/posts/${postId}`, {
    data: {
      status: 'archived'
    }
  });
  expect(patchResponse.status()).toBe(200);

  // DELETE
  const deleteResponse = await request.delete(`https://api.example.com/posts/${postId}`, {
    data: {
      reason: 'test cleanup'
    }
  });
  expect(deleteResponse.status()).toBe(204);
});

Simple Object

{
  "username": "john_doe",
  "password": "securePass123",
  "email": "john@example.com"
}

Nested Objects

{
  "user": {
    "name": "Nguyễn Văn A",
    "email": "vana@example.com",
    "profile": {
      "bio": "Software developer",
      "website": "https://example.com",
      "social": {
        "twitter": "@johndoe",
        "github": "johndoe"
      }
    }
  },
  "preferences": {
    "theme": "dark",
    "language": "vi",
    "notifications": {
      "email": true,
      "push": false
    }
  }
}
⚠️ Tránh nested quá sâu:
  • Max 3-4 levels nesting
  • Quá sâu → khó đọc, khó maintain
  • Consider flatten structure nếu có thể

Arrays

{
  "name": "Shopping Cart",
  "items": [
    {
      "product_id": 101,
      "name": "Laptop",
      "quantity": 1,
      "price": 15000000
    },
    {
      "product_id": 202,
      "name": "Mouse",
      "quantity": 2,
      "price": 200000
    }
  ],
  "tags": ["electronics", "computers", "accessories"],
  "total": 15400000
}

Bulk Operations

{
  "operations": [
    {
      "action": "create",
      "type": "user",
      "data": {
        "name": "User 1",
        "email": "user1@example.com"
      }
    },
    {
      "action": "update",
      "type": "user",
      "id": 123,
      "data": {
        "status": "active"
      }
    },
    {
      "action": "delete",
      "type": "user",
      "id": 456
    }
  ]
}

Test Bulk Operations

test('Bulk operations', async ({ request }) => {
  const response = await request.post('https://api.example.com/bulk', {
    data: {
      operations: [
        {
          action: 'create',
          type: 'post',
          data: { title: 'Post 1', content: 'Content 1' }
        },
        {
          action: 'create',
          type: 'post',
          data: { title: 'Post 2', content: 'Content 2' }
        },
        {
          action: 'update',
          type: 'post',
          id: 100,
          data: { status: 'published' }
        }
      ]
    }
  });

  expect(response.status()).toBe(200);

  const result = await response.json();
  expect(result.success).toBe(true);
  expect(result.results).toHaveLength(3);
});

So sánh Request Body với các phương pháp khác

Feature Request Body Query Params Path Params Headers
Data size Large (MB+) Small (~2KB) Very small Small
Visibility Hidden Visible in URL Visible in URL Hidden
Use case Create/Update data Filter/Search Resource ID Metadata
HTTP Methods POST/PUT/PATCH GET (mainly) All methods All methods
Cacheable No Yes Yes Depends
Bookmarkable No Yes Yes No
Security Better Logged/visible Logged/visible Better
Complex data ✓ Yes ✗ Limited ✗ No ✗ No
Nested objects ✓ Yes Limited ✗ No ✗ No
Arrays ✓ Yes Limited ✗ No ✗ No
File upload ✓ Yes ✗ No ✗ No ✗ No

Khi nào dùng Request Body?

  • Create/Update operations: POST, PUT, PATCH
  • Large data: Dữ liệu lớn không fit trong URL
  • Complex structures: Nested objects, arrays
  • Sensitive information: Passwords, credit cards, personal data
  • File uploads: Images, documents, videos
  • Bulk operations: Multiple records cùng lúc
  • Complex search: Advanced filters không fit trong query params

Khi KHÔNG nên dùng Request Body?

  • GET requests: Dùng query parameters
  • Simple filtering: Dùng query parameters
  • Resource identification: Dùng path parameters
  • Metadata: Dùng headers
  • Cacheable data: Query params cho URLs có thể cache

Ví dụ: Chọn phương pháp phù hợp

// ✓ Path param: Resource ID
GET /api/users/123

// ✓ Query params: Simple filter
GET /api/products?category=laptop&sort=price

// ✓ Headers: Auth & metadata
GET /api/users/me
Authorization: Bearer token123
Accept: application/json

// ✓ Request Body: Create with complex data
POST /api/orders
{
  "customer": {...},
  "items": [...],
  "shipping_address": {...}
}

// ✓ Request Body: Complex search
POST /api/products/search
{
  "filters": [
    {"field": "category", "op": "in", "value": ["laptop", "phone"]},
    {"field": "price", "op": "between", "value": [1000000, 5000000]}
  ],
  "sort": [{"field": "price", "order": "asc"}],
  "page": 1,
  "limit": 20
}

Structure Best Practices

  • Consistent naming: Chọn camelCase hoặc snake_case và dùng nhất quán
  • Clear hierarchy: Group related fields together
  • Avoid deep nesting: Max 3-4 levels
  • Use arrays for collections: Không dùng object với numeric keys
  • Explicit types: String, number, boolean, null (không dùng "true" string cho boolean)
  • ISO dates: YYYY-MM-DD hoặc ISO 8601 datetime

Validation Best Practices

Required vs Optional Fields:

// Document clearly
{
  "name": "John Doe",        // required
  "email": "john@example.com", // required
  "phone": "0123456789",     // optional
  "address": {               // optional
    "city": "Hanoi",
    "country": "Vietnam"
  }
}

Data Type Validation:

// ✓ Validate types
{
  "age": 25,              // number, not "25" string
  "active": true,         // boolean, not "true" string
  "email": "valid@email.com", // valid email format
  "phone": "0123456789",  // valid phone format
  "date": "2025-01-15"    // valid ISO date
}

Length & Range Limits:

// Set reasonable limits
{
  "name": "...",      // max 100 characters
  "email": "...",     // max 255 characters
  "bio": "...",       // max 1000 characters
  "age": 25,          // range: 0-150
  "quantity": 5,      // range: 1-999
  "tags": ["..."]     // max 10 items
}

Security Best Practices

1. Validate & Sanitize Input

// PHP validation example
$data = json_decode(file_get_contents('php://input'), true);

// Validate required fields
$errors = [];
if (empty($data['email'])) {
    $errors['email'] = 'Email is required';
}

// Validate format
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
    $errors['email'] = 'Invalid email format';
}

// Validate length
if (strlen($data['name']) > 100) {
    $errors['name'] = 'Name too long (max 100 chars)';
}

// Sanitize HTML
$bio = htmlspecialchars($data['bio'], ENT_QUOTES, 'UTF-8');

if (!empty($errors)) {
    http_response_code(400);
    echo json_encode(['errors' => $errors]);
    exit;
}

2. SQL Injection Prevention

// ✗ Nguy hiểm!
$name = $data['name'];
$sql = "SELECT * FROM users WHERE name = '$name'";
// Attacker gửi: ' OR '1'='1

// ✓ An toàn: Parameterized query
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$data['name']]);

3. XSS Prevention

// ✗ Nguy hiểm!
$comment = $data['comment'];
echo "<div>$comment</div>";
// Attacker gửi: <script>alert('xss')</script>

// ✓ An toàn: Escape HTML
$comment = htmlspecialchars($data['comment'], ENT_QUOTES, 'UTF-8');
echo "<div>$comment</div>";

4. File Upload Validation

// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($_FILES['file']['type'], $allowedTypes)) {
    die('Invalid file type');
}

// Validate file size (max 5MB)
$maxSize = 5 * 1024 * 1024;
if ($_FILES['file']['size'] > $maxSize) {
    die('File too large');
}

// Validate file extension
$allowedExts = ['jpg', 'jpeg', 'png', 'pdf'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
    die('Invalid file extension');
}

// Generate unique filename
$newName = uniqid() . '.' . $ext;

// Don't use original filename!

5. Size Limits

// Set request body size limit
// PHP: post_max_size, upload_max_filesize in php.ini
// Nginx: client_max_body_size

// Validate in code
$json = file_get_contents('php://input');
if (strlen($json) > 1024 * 1024) {  // 1MB limit
    http_response_code(413);  // Payload Too Large
    echo json_encode(['error' => 'Request too large']);
    exit;
}

Performance Best Practices

  • ✓ Keep body size reasonable (compress nếu cần)
  • ✓ Avoid sending unnecessary fields
  • ✓ Use pagination cho large datasets
  • ✓ Batch operations khi có thể (bulk create/update)
  • ✓ Compress responses với gzip
  • ✓ Set appropriate timeouts

Error Handling

Clear validation errors:

// ✓ Good error response
{
  "error": "Validation failed",
  "errors": {
    "email": "Invalid email format",
    "age": "Age must be between 0 and 150",
    "phone": "Phone number is required"
  },
  "status": 400
}

// ✗ Bad error response
{
  "error": "Bad request"
}

HTTP Status Codes:

  • 400 Bad Request - Invalid body format, validation errors
  • 413 Payload Too Large - Body quá lớn
  • 415 Unsupported Media Type - Wrong Content-Type
  • 422 Unprocessable Entity - Valid format nhưng invalid data

Pattern 1: Pagination trong POST Request

POST /api/products/search
{
  "filters": {
    "category": "laptop",
    "price_min": 10000000,
    "price_max": 20000000
  },
  "sort": {
    "field": "price",
    "order": "asc"
  },
  "page": 2,
  "limit": 20
}

Pattern 2: Search với Complex Filters

POST /api/search
{
  "query": "gaming laptop",
  "filters": [
    {
      "field": "category",
      "operator": "eq",
      "value": "laptop"
    },
    {
      "field": "ram",
      "operator": "gte",
      "value": 16
    },
    {
      "field": "brand",
      "operator": "in",
      "value": ["dell", "hp", "lenovo"]
    }
  ],
  "sort": [
    {"field": "relevance", "order": "desc"},
    {"field": "price", "order": "asc"}
  ],
  "page": 1,
  "limit": 20
}

Pattern 3: JSON Patch (RFC 6902)

PATCH /api/users/123
Content-Type: application/json-patch+json

[
  {"op": "replace", "path": "/email", "value": "newemail@example.com"},
  {"op": "add", "path": "/tags/-", "value": "premium"},
  {"op": "remove", "path": "/old_field"}
]

Pattern 4: File Upload với Metadata

POST /api/documents
Content-Type: multipart/form-data

title=My Document
description=Important document
category=reports
tags=Q1,2025,financial
visibility=private
file=[binary data]

Complete Testing Example

import { test, expect } from '@playwright/test';

test.describe('Request Body Testing', () => {

  test('POST - Create user with validation', async ({ request }) => {
    const response = await request.post('https://api.example.com/users', {
      data: {
        name: 'Nguyễn Văn A',
        email: 'vana@example.com',
        age: 25,
        address: {
          city: 'Hà Nội',
          country: 'Vietnam'
        }
      }
    });

    expect(response.status()).toBe(201);

    const user = await response.json();
    expect(user).toHaveProperty('id');
    expect(user.name).toBe('Nguyễn Văn A');
  });

  test('POST - Validation errors', async ({ request }) => {
    const response = await request.post('https://api.example.com/users', {
      data: {
        name: '',  // Empty name
        email: 'invalid-email',  // Invalid email
        age: 200  // Invalid age
      }
    });

    expect(response.status()).toBe(400);

    const error = await response.json();
    expect(error).toHaveProperty('errors');
    expect(error.errors).toHaveProperty('name');
    expect(error.errors).toHaveProperty('email');
    expect(error.errors).toHaveProperty('age');
  });

  test('POST - Complex nested structure', async ({ request }) => {
    const response = await request.post('https://api.example.com/orders', {
      data: {
        customer: {
          name: 'Nguyễn Văn A',
          email: 'vana@example.com',
          phone: '0123456789'
        },
        items: [
          {
            product_id: 101,
            quantity: 2,
            price: 500000,
            options: {
              color: 'black',
              size: 'L'
            }
          },
          {
            product_id: 202,
            quantity: 1,
            price: 1000000
          }
        ],
        shipping_address: {
          street: '123 Main St',
          city: 'Hà Nội',
          district: 'Ba Đình',
          country: 'Vietnam',
          postal_code: '100000'
        },
        payment_method: 'credit_card',
        notes: 'Giao giờ hành chính'
      }
    });

    expect(response.status()).toBe(201);

    const order = await response.json();
    expect(order).toHaveProperty('id');
    expect(order).toHaveProperty('total');
    expect(order.items).toHaveLength(2);
  });

  test('PATCH - Partial update', async ({ request }) => {
    const response = await request.patch('https://api.example.com/users/123', {
      data: {
        status: 'active',
        last_login: new Date().toISOString()
      }
    });

    expect(response.status()).toBe(200);
  });

  test('PUT - Full update', async ({ request }) => {
    const response = await request.put('https://api.example.com/posts/456', {
      data: {
        title: 'Updated Title',
        content: 'Updated content...',
        status: 'published',
        tags: ['api', 'testing'],
        publish_date: '2025-02-01'
      }
    });

    expect(response.status()).toBe(200);
  });

  test('File upload', async ({ request }) => {
    const fileContent = Buffer.from('test file content');

    const response = await request.post('https://api.example.com/upload', {
      multipart: {
        title: 'Test Document',
        description: 'Test description',
        category: 'test',
        file: {
          name: 'test.txt',
          mimeType: 'text/plain',
          buffer: fileContent
        }
      }
    });

    expect(response.status()).toBe(201);

    const result = await response.json();
    expect(result).toHaveProperty('file_id');
    expect(result).toHaveProperty('url');
  });

  test('Bulk operations', async ({ request }) => {
    const response = await request.post('https://api.example.com/bulk', {
      data: {
        operations: [
          {
            action: 'create',
            type: 'post',
            data: {
              title: 'Post 1',
              content: 'Content 1'
            }
          },
          {
            action: 'create',
            type: 'post',
            data: {
              title: 'Post 2',
              content: 'Content 2'
            }
          },
          {
            action: 'update',
            type: 'post',
            id: 100,
            data: {
              status: 'published'
            }
          },
          {
            action: 'delete',
            type: 'post',
            id: 99
          }
        ]
      }
    });

    expect(response.status()).toBe(200);

    const result = await response.json();
    expect(result.results).toHaveLength(4);
    expect(result.results[0].success).toBe(true);
  });

  test('Large JSON payload', async ({ request }) => {
    // Create large dataset
    const items = [];
    for (let i = 0; i < 1000; i++) {
      items.push({
        id: i,
        name: `Item ${i}`,
        value: Math.random() * 1000
      });
    }

    const response = await request.post('https://api.example.com/bulk-import', {
      data: {
        items: items
      }
    });

    expect(response.status()).toBe(202);  // Accepted for processing
  });

});