← Trở về trang chủ

API Response Body

Định nghĩa

Response Body là phần dữ liệu mà server trả về cho client sau khi xử lý một HTTP request. Đây là nơi chứa thông tin chính mà client cần.

Cấu trúc HTTP Response

HTTP/1.1 200 OK                    ← Status Line
Content-Type: application/json     ← Headers
Content-Length: 85
Date: Mon, 15 Jan 2025 10:30:00 GMT

{                                  ← Response Body (bắt đầu)
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com"
}                                  ← Response Body (kết thúc)

Các thành phần của HTTP Response

Thành phần Mô tả Ví dụ
Status Line HTTP version, status code, status text HTTP/1.1 200 OK
Headers Metadata về response Content-Type: application/json
Body Dữ liệu chính được trả về {"id": 1, "name": "John"}

Khi nào Response có Body?

Có Response Body

  • GET requests: Trả về resource data
  • POST requests: Trả về created resource
  • PUT/PATCH requests: Trả về updated resource
  • Error responses: Trả về error details

Không có Response Body (hoặc empty)

  • 204 No Content: Success nhưng không có data trả về
  • DELETE success: Thường trả về 204
  • HEAD requests: Chỉ trả về headers
  • 304 Not Modified: Client dùng cached version

Ví dụ với Playwright

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

test('Đọc response body', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');

  // Kiểm tra status
  expect(response.status()).toBe(200);

  // Đọc body dạng JSON
  const body = await response.json();

  // Verify body data
  expect(body).toHaveProperty('id');
  expect(body).toHaveProperty('name');
  expect(body).toHaveProperty('email');

  console.log('Response body:', body);
});

test('Response không có body', async ({ request }) => {
  const response = await request.delete('https://api.example.com/users/1');

  // 204 No Content - không có body
  expect(response.status()).toBe(204);

  // Body rỗng
  const text = await response.text();
  expect(text).toBe('');
});

JSON là gì?

JSON là format phổ biến nhất cho API responses. Dễ đọc, dễ parse, và được hỗ trợ bởi hầu hết các ngôn ngữ.

Content-Type: application/json

Cấu trúc JSON cơ bản

// Object
{
  "key": "value",
  "number": 123,
  "boolean": true,
  "null_value": null
}

// Array
[
  {"id": 1, "name": "Item 1"},
  {"id": 2, "name": "Item 2"}
]

// Nested structure
{
  "user": {
    "id": 1,
    "name": "John",
    "address": {
      "city": "Hanoi",
      "country": "Vietnam"
    }
  },
  "orders": [
    {"id": 101, "total": 50000},
    {"id": 102, "total": 75000}
  ]
}

JSON Data Types

Type Ví dụ Ghi chú
String "Hello World" Phải dùng double quotes
Number 123, 45.67, -89 Integer hoặc float
Boolean true, false Lowercase
Null null Lowercase
Array [1, 2, 3] Ordered list
Object {"key": "value"} Key-value pairs

Parse JSON với Playwright

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

test('Parse JSON response', async ({ request }) => {
  const response = await request.get('https://api.example.com/users');

  // Verify Content-Type
  expect(response.headers()['content-type']).toContain('application/json');

  // Parse JSON
  const users = await response.json();

  // Verify structure
  expect(users).toBeInstanceOf(Array);
  expect(users.length).toBeGreaterThan(0);

  // Verify first user
  const firstUser = users[0];
  expect(firstUser).toHaveProperty('id');
  expect(typeof firstUser.id).toBe('number');
  expect(firstUser).toHaveProperty('name');
  expect(typeof firstUser.name).toBe('string');
});

test('Handle nested JSON', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  const user = await response.json();

  // Access nested properties
  expect(user.address.city).toBe('Hanoi');
  expect(user.orders[0].total).toBeGreaterThan(0);
});
⚠️ Lưu ý JSON:
  • Keys phải là strings (có double quotes)
  • Không có trailing comma
  • Không có comments
  • Dates thường là ISO 8601 strings

XML là gì?

XML là format cũ hơn, vẫn được dùng trong enterprise systems, SOAP APIs, và một số legacy systems.

Content-Type: application/xml hoặc text/xml

Cấu trúc XML

<?xml version="1.0" encoding="UTF-8"?>
<user>
  <id>1</id>
  <name>John Doe</name>
  <email>john@example.com</email>
  <address>
    <city>Hanoi</city>
    <country>Vietnam</country>
  </address>
  <orders>
    <order id="101">
      <total>50000</total>
    </order>
    <order id="102">
      <total>75000</total>
    </order>
  </orders>
</user>

Parse XML với Playwright

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

test('Parse XML response', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1.xml');

  // Verify Content-Type
  const contentType = response.headers()['content-type'];
  expect(contentType).toMatch(/xml/);

  // Get as text
  const xmlText = await response.text();

  // Simple verification using regex (hoặc dùng XML parser library)
  expect(xmlText).toContain('<user>');
  expect(xmlText).toContain('<name>John Doe</name>');
});

// Với xml2js library (cần install)
import { parseStringPromise } from 'xml2js';

test('Parse XML with library', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1.xml');
  const xmlText = await response.text();

  // Parse XML to JSON
  const result = await parseStringPromise(xmlText);

  expect(result.user.name[0]).toBe('John Doe');
  expect(result.user.email[0]).toBe('john@example.com');
});

So sánh JSON vs XML

Aspect JSON XML
Readability Dễ đọc hơn Verbose hơn
Size Nhỏ hơn Lớn hơn
Parsing Native trong JS Cần library
Schema JSON Schema XSD, DTD
Use case REST APIs, Web SOAP, Enterprise

HTML Response

Content-Type: text/html

<!DOCTYPE html>
<html>
<head><title>User Profile</title></head>
<body>
  <h1>John Doe</h1>
  <p>Email: john@example.com</p>
</body>
</html>

Plain Text Response

Content-Type: text/plain

OK
User created successfully
Error: Invalid input

Binary Response

Content-Types:

  • application/pdf - PDF files
  • image/png, image/jpeg - Images
  • application/octet-stream - Generic binary
  • application/zip - ZIP files

Handle different formats với Playwright

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

// HTML response
test('Parse HTML response', async ({ request }) => {
  const response = await request.get('https://example.com/page');
  const html = await response.text();

  expect(html).toContain('<title>');
  expect(html).toContain('</html>');
});

// Plain text response
test('Parse plain text response', async ({ request }) => {
  const response = await request.get('https://api.example.com/health');
  const text = await response.text();

  expect(text).toBe('OK');
});

// Binary response (image)
test('Download image', async ({ request }) => {
  const response = await request.get('https://api.example.com/avatar/1');

  expect(response.headers()['content-type']).toContain('image/');

  // Get as buffer
  const buffer = await response.body();
  expect(buffer.length).toBeGreaterThan(0);
});

// PDF response
test('Download PDF', async ({ request }) => {
  const response = await request.get('https://api.example.com/reports/123.pdf');

  expect(response.headers()['content-type']).toContain('application/pdf');

  const buffer = await response.body();
  // PDF files start with %PDF
  expect(buffer.toString().startsWith('%PDF')).toBeTruthy();
});

Direct Object

// Simple: Trả về object trực tiếp
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "created_at": "2025-01-15T10:30:00Z"
}

Wrapped Object

// Wrapped: Object nằm trong "data" field
{
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "meta": {
    "request_id": "abc123",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

With Related Data

// Include related resources
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "department": {
    "id": 10,
    "name": "Engineering"
  },
  "manager": {
    "id": 5,
    "name": "Jane Smith"
  },
  "roles": ["admin", "developer"]
}

Test Single Resource

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

test('GET single user', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');

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

  const user = await response.json();

  // Verify required fields
  expect(user).toHaveProperty('id', 1);
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('email');

  // Verify data types
  expect(typeof user.id).toBe('number');
  expect(typeof user.name).toBe('string');
  expect(user.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);

  // Verify optional related data
  if (user.department) {
    expect(user.department).toHaveProperty('id');
    expect(user.department).toHaveProperty('name');
  }
});

Simple Array

// Trả về array trực tiếp
[
  {"id": 1, "name": "John"},
  {"id": 2, "name": "Jane"},
  {"id": 3, "name": "Bob"}
]

Wrapped Array với Pagination

// Array trong "data" field + pagination info
{
  "data": [
    {"id": 1, "name": "John"},
    {"id": 2, "name": "Jane"},
    {"id": 3, "name": "Bob"}
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 157,
    "total_pages": 8
  }
}

With Links (HATEOAS style)

{
  "data": [...],
  "links": {
    "self": "/api/users?page=2",
    "first": "/api/users?page=1",
    "prev": "/api/users?page=1",
    "next": "/api/users?page=3",
    "last": "/api/users?page=8"
  },
  "meta": {
    "current_page": 2,
    "per_page": 20,
    "total": 157,
    "total_pages": 8
  }
}

Cursor-based Pagination

{
  "data": [...],
  "next_cursor": "eyJpZCI6MTAwfQ==",
  "has_more": true
}

Test Collection Response

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

test('GET users list with pagination', async ({ request }) => {
  const response = await request.get('https://api.example.com/users', {
    params: { page: 1, limit: 20 }
  });

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

  const result = await response.json();

  // Verify data array
  expect(result.data).toBeInstanceOf(Array);
  expect(result.data.length).toBeLessThanOrEqual(20);

  // Verify each item in array
  result.data.forEach(user => {
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
  });

  // Verify pagination
  expect(result.pagination).toHaveProperty('page', 1);
  expect(result.pagination).toHaveProperty('total');
  expect(result.pagination).toHaveProperty('total_pages');
});

test('Verify empty collection', async ({ request }) => {
  const response = await request.get('https://api.example.com/users', {
    params: { status: 'nonexistent' }
  });

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

  const result = await response.json();

  // Empty array, not 404
  expect(result.data).toEqual([]);
  expect(result.pagination.total).toBe(0);
});

Standard Envelope

Tất cả responses đều có cùng structure:

// Success response
{
  "success": true,
  "data": {
    "id": 1,
    "name": "John Doe"
  },
  "message": "User retrieved successfully",
  "timestamp": "2025-01-15T10:30:00Z"
}

// Error response
{
  "success": false,
  "data": null,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with ID 999 not found"
  },
  "timestamp": "2025-01-15T10:30:00Z"
}

JSend Specification

// Success
{
  "status": "success",
  "data": {
    "user": {"id": 1, "name": "John"}
  }
}

// Fail (validation error)
{
  "status": "fail",
  "data": {
    "email": "Invalid email format",
    "password": "Password too short"
  }
}

// Error (server error)
{
  "status": "error",
  "message": "Internal server error",
  "code": 500
}

Test Envelope Pattern

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

test('Verify envelope structure - success', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');

  const body = await response.json();

  // Verify envelope structure
  expect(body).toHaveProperty('success', true);
  expect(body).toHaveProperty('data');
  expect(body).toHaveProperty('timestamp');

  // Verify actual data
  expect(body.data).toHaveProperty('id', 1);
});

test('Verify envelope structure - error', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/99999');

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

  const body = await response.json();

  // Verify error envelope
  expect(body).toHaveProperty('success', false);
  expect(body).toHaveProperty('error');
  expect(body.error).toHaveProperty('code');
  expect(body.error).toHaveProperty('message');
  expect(body.data).toBeNull();
});

Ưu điểm của Envelope Pattern

  • ✓ Consistent structure cho tất cả responses
  • ✓ Dễ handle errors uniformly
  • ✓ Có thể include metadata (timestamp, request_id, ...)
  • ✓ Client code đơn giản hơn

Nhược điểm

  • ✗ Thêm overhead (envelope wrapper)
  • ✗ Không theo REST purist approach
  • ✗ HTTP status codes có thể bị ignore

2xx Success Responses

Status Tên Response Body Use case
200 OK Có data GET, PUT, PATCH success
201 Created Created resource POST success
202 Accepted Status info Async processing started
204 No Content Empty DELETE success

Success Response Examples

// 200 OK - GET user
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com"
}

// 201 Created - POST new user
{
  "id": 10,
  "name": "New User",
  "email": "newuser@example.com",
  "created_at": "2025-01-15T10:30:00Z"
}

// 202 Accepted - Async job
{
  "job_id": "abc123",
  "status": "processing",
  "check_url": "/api/jobs/abc123"
}

// 204 No Content - DELETE
(empty body)

4xx Client Error Responses

Status Tên Response Body Use case
400 Bad Request Validation errors Invalid input
401 Unauthorized Auth error Missing/invalid token
403 Forbidden Permission error Không có quyền
404 Not Found Error message Resource không tồn tại
409 Conflict Conflict details Duplicate, conflict
422 Unprocessable Entity Validation errors Semantic errors
429 Too Many Requests Rate limit info Rate limited

4xx Error Response Examples

// 400 Bad Request
{
  "error": "Bad Request",
  "message": "Invalid JSON format",
  "details": "Unexpected token at position 15"
}

// 401 Unauthorized
{
  "error": "Unauthorized",
  "message": "Invalid or expired access token",
  "code": "TOKEN_EXPIRED"
}

// 403 Forbidden
{
  "error": "Forbidden",
  "message": "You don't have permission to access this resource",
  "required_role": "admin"
}

// 404 Not Found
{
  "error": "Not Found",
  "message": "User with ID 999 not found"
}

// 422 Validation Error
{
  "error": "Validation Error",
  "message": "Input validation failed",
  "errors": [
    {"field": "email", "message": "Invalid email format"},
    {"field": "password", "message": "Must be at least 8 characters"}
  ]
}

// 429 Rate Limited
{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded",
  "retry_after": 60
}

5xx Server Error Responses

Status Tên Response Body
500 Internal Server Error Generic error (không expose details)
502 Bad Gateway Upstream error
503 Service Unavailable Maintenance info
504 Gateway Timeout Timeout info

5xx Error Response Examples

// 500 Internal Server Error
{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "request_id": "abc123"  // Để support team trace
}

// 503 Service Unavailable
{
  "error": "Service Unavailable",
  "message": "Service is under maintenance",
  "retry_after": 3600
}

Test Status Codes với Playwright

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

test('Test various status codes', async ({ request }) => {
  // 200 OK
  const res200 = await request.get('https://api.example.com/users/1');
  expect(res200.status()).toBe(200);
  expect(res200.ok()).toBeTruthy();

  // 201 Created
  const res201 = await request.post('https://api.example.com/users', {
    data: { name: 'New User', email: 'new@example.com' }
  });
  expect(res201.status()).toBe(201);
  const created = await res201.json();
  expect(created).toHaveProperty('id');

  // 204 No Content
  const res204 = await request.delete('https://api.example.com/users/1');
  expect(res204.status()).toBe(204);
  expect(await res204.text()).toBe('');

  // 404 Not Found
  const res404 = await request.get('https://api.example.com/users/99999');
  expect(res404.status()).toBe(404);
  expect(res404.ok()).toBeFalsy();
  const error404 = await res404.json();
  expect(error404).toHaveProperty('error');

  // 422 Validation Error
  const res422 = await request.post('https://api.example.com/users', {
    data: { name: '', email: 'invalid-email' }
  });
  expect(res422.status()).toBe(422);
  const error422 = await res422.json();
  expect(error422.errors).toBeInstanceOf(Array);
});

Simple Error

{
  "error": "Not Found",
  "message": "User with ID 999 not found"
}

Detailed Error

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with ID 999 not found",
    "status": 404,
    "timestamp": "2025-01-15T10:30:00Z",
    "path": "/api/users/999",
    "request_id": "abc123"
  }
}

RFC 7807 Problem Details

{
  "type": "https://api.example.com/errors/not-found",
  "title": "Resource Not Found",
  "status": 404,
  "detail": "User with ID 999 does not exist",
  "instance": "/api/users/999"
}

Error Response Fields

Field Mô tả Required?
error / code Error type/code Yes
message Human-readable message Yes
status HTTP status code Optional
details Additional info Optional
request_id For debugging/support Recommended

Array of Errors

{
  "error": "Validation Error",
  "message": "Input validation failed",
  "errors": [
    {
      "field": "email",
      "message": "Invalid email format",
      "code": "INVALID_FORMAT"
    },
    {
      "field": "password",
      "message": "Must be at least 8 characters",
      "code": "TOO_SHORT",
      "min_length": 8
    },
    {
      "field": "age",
      "message": "Must be a positive number",
      "code": "INVALID_VALUE"
    }
  ]
}

Object Format (field as key)

{
  "error": "Validation Error",
  "errors": {
    "email": ["Invalid email format", "Email already exists"],
    "password": ["Must be at least 8 characters"],
    "age": ["Must be a positive number"]
  }
}

Nested Validation Errors

{
  "error": "Validation Error",
  "errors": {
    "user.name": ["Name is required"],
    "user.address.city": ["City is required"],
    "items[0].quantity": ["Must be at least 1"],
    "items[1].price": ["Invalid price format"]
  }
}

Test Validation Errors

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

test('Test validation errors', async ({ request }) => {
  const response = await request.post('https://api.example.com/users', {
    data: {
      name: '',           // Empty - invalid
      email: 'not-email', // Invalid format
      age: -5             // Negative - invalid
    }
  });

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

  const body = await response.json();

  // Verify error structure
  expect(body).toHaveProperty('error', 'Validation Error');
  expect(body).toHaveProperty('errors');
  expect(body.errors).toBeInstanceOf(Array);

  // Verify specific errors
  const fieldErrors = body.errors.map(e => e.field);
  expect(fieldErrors).toContain('name');
  expect(fieldErrors).toContain('email');
  expect(fieldErrors).toContain('age');

  // Find specific error
  const emailError = body.errors.find(e => e.field === 'email');
  expect(emailError.message).toContain('email');
});

401 Unauthorized Examples

// Missing token
{
  "error": "Unauthorized",
  "message": "Authentication required",
  "code": "AUTH_REQUIRED"
}

// Invalid token
{
  "error": "Unauthorized",
  "message": "Invalid access token",
  "code": "INVALID_TOKEN"
}

// Expired token
{
  "error": "Unauthorized",
  "message": "Access token has expired",
  "code": "TOKEN_EXPIRED",
  "expired_at": "2025-01-15T10:30:00Z"
}

403 Forbidden Examples

// Insufficient permissions
{
  "error": "Forbidden",
  "message": "You don't have permission to perform this action",
  "code": "INSUFFICIENT_PERMISSIONS",
  "required_permission": "users:delete"
}

// Role-based restriction
{
  "error": "Forbidden",
  "message": "Admin role required",
  "code": "ROLE_REQUIRED",
  "required_role": "admin",
  "current_role": "user"
}

// Resource ownership
{
  "error": "Forbidden",
  "message": "You can only modify your own resources",
  "code": "OWNERSHIP_REQUIRED"
}

Test Auth Errors

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

test('Test 401 - No token', async ({ request }) => {
  const response = await request.get('https://api.example.com/protected');

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

  const body = await response.json();
  expect(body.code).toBe('AUTH_REQUIRED');
});

test('Test 401 - Invalid token', async ({ request }) => {
  const response = await request.get('https://api.example.com/protected', {
    headers: {
      'Authorization': 'Bearer invalid-token'
    }
  });

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

  const body = await response.json();
  expect(body.code).toBe('INVALID_TOKEN');
});

test('Test 403 - Insufficient permissions', async ({ request }) => {
  // Login as normal user
  const loginRes = await request.post('https://api.example.com/login', {
    data: { email: 'user@example.com', password: 'password' }
  });
  const { token } = await loginRes.json();

  // Try to access admin-only resource
  const response = await request.delete('https://api.example.com/admin/users/1', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

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

  const body = await response.json();
  expect(body.code).toBe('INSUFFICIENT_PERMISSIONS');
});

Các phương thức đọc Response Body

Method Return Type Use case
response.json() Object/Array JSON responses
response.text() String Text, HTML, XML
response.body() Buffer Binary data (images, PDFs)

Basic Parsing

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

test('Parse JSON response', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');

  // Parse as JSON
  const user = await response.json();

  expect(user.id).toBe(1);
  expect(user.name).toBeDefined();
});

test('Parse text response', async ({ request }) => {
  const response = await request.get('https://api.example.com/health');

  // Parse as text
  const text = await response.text();

  expect(text).toBe('OK');
});

test('Handle binary response', async ({ request }) => {
  const response = await request.get('https://api.example.com/files/image.png');

  // Get as buffer
  const buffer = await response.body();

  expect(buffer.length).toBeGreaterThan(0);
  // PNG magic bytes
  expect(buffer[0]).toBe(0x89);
  expect(buffer[1]).toBe(0x50);  // 'P'
  expect(buffer[2]).toBe(0x4E);  // 'N'
  expect(buffer[3]).toBe(0x47);  // 'G'
});

Verify Response Structure

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

test('Verify complete response structure', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  const user = await response.json();

  // Check required fields exist
  expect(user).toHaveProperty('id');
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('email');
  expect(user).toHaveProperty('created_at');

  // Check data types
  expect(typeof user.id).toBe('number');
  expect(typeof user.name).toBe('string');
  expect(typeof user.email).toBe('string');

  // Check format
  expect(user.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  expect(user.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);

  // Check value ranges
  expect(user.id).toBeGreaterThan(0);
  expect(user.name.length).toBeGreaterThan(0);
});

test('Verify array response', async ({ request }) => {
  const response = await request.get('https://api.example.com/users');
  const result = await response.json();

  // Verify pagination structure
  expect(result).toHaveProperty('data');
  expect(result).toHaveProperty('pagination');

  // Verify data array
  expect(Array.isArray(result.data)).toBeTruthy();

  // Verify each item
  for (const user of result.data) {
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
    expect(typeof user.id).toBe('number');
  }

  // Verify pagination
  expect(result.pagination).toMatchObject({
    page: expect.any(Number),
    per_page: expect.any(Number),
    total: expect.any(Number)
  });
});

Handle Optional Fields

test('Handle optional fields', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  const user = await response.json();

  // Required fields
  expect(user.id).toBeDefined();
  expect(user.name).toBeDefined();

  // Optional fields - check if exists before asserting
  if (user.phone) {
    expect(user.phone).toMatch(/^\+?[\d\s-]+$/);
  }

  if (user.address) {
    expect(user.address).toHaveProperty('city');
    expect(user.address).toHaveProperty('country');
  }

  // Or use optional chaining
  expect(user.profile?.bio ?? '').toBeDefined();
});

Compare Response với Expected Data

test('Compare response với expected data', async ({ request }) => {
  // Create user
  const createRes = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com'
    }
  });

  const created = await createRes.json();

  // Verify response matches input
  expect(created).toMatchObject({
    name: 'John Doe',
    email: 'john@example.com'
  });

  // Verify server-generated fields
  expect(created.id).toBeDefined();
  expect(created.created_at).toBeDefined();
});

test('Snapshot testing', async ({ request }) => {
  const response = await request.get('https://api.example.com/config');
  const config = await response.json();

  // Compare với snapshot (cần setup)
  expect(config).toMatchSnapshot();
});

Extract và Reuse Data

test('Extract và reuse data', async ({ request }) => {
  // Step 1: Create user
  const createRes = await request.post('https://api.example.com/users', {
    data: { name: 'Test User', email: 'test@example.com' }
  });
  const { id: userId } = await createRes.json();

  // Step 2: Create order for user
  const orderRes = await request.post('https://api.example.com/orders', {
    data: {
      user_id: userId,  // Reuse userId
      items: [{ product_id: 1, quantity: 2 }]
    }
  });
  const { id: orderId } = await orderRes.json();

  // Step 3: Get order details
  const getRes = await request.get(`https://api.example.com/orders/${orderId}`);
  const order = await getRes.json();

  expect(order.user_id).toBe(userId);
  expect(order.items.length).toBe(1);
});

1. Consistent Response Structure

// ✓ Good: Consistent structure cho tất cả endpoints
// Success
{
  "data": {...},
  "meta": {...}
}

// Error
{
  "error": {...}
}

// ✗ Bad: Mỗi endpoint khác nhau
/users → [...]
/orders → {"orders": [...]}
/products → {"data": {"items": [...]}}

2. Meaningful Error Messages

// ✓ Good: Clear, actionable message
{
  "error": "Validation Error",
  "message": "Email format is invalid. Expected format: user@domain.com",
  "field": "email",
  "provided_value": "not-an-email"
}

// ✗ Bad: Generic, unhelpful
{
  "error": "Bad request"
}

3. Include Request ID

// Mỗi response nên có request_id để debug
{
  "data": {...},
  "meta": {
    "request_id": "req_abc123xyz"
  }
}

// Đặc biệt quan trọng cho errors
{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "request_id": "req_abc123xyz"  // Support team có thể trace
}

4. Proper HTTP Status Codes

// ✓ Good: Đúng status code
200 OK - GET success
201 Created - POST success (resource created)
204 No Content - DELETE success
400 Bad Request - Invalid input format
401 Unauthorized - Authentication required
403 Forbidden - Permission denied
404 Not Found - Resource not found
422 Unprocessable Entity - Validation failed
500 Internal Server Error - Server error

// ✗ Bad: Sai status code
200 OK với body {"error": "Not found"}  // Nên là 404
500 cho validation errors  // Nên là 400/422

5. Date/Time Format

// ✓ Good: ISO 8601 với timezone
{
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T14:45:30+07:00"
}

// ✗ Bad: Ambiguous formats
{
  "created_at": "15/01/2025",  // DD/MM hay MM/DD?
  "updated_at": 1705312200     // Unix timestamp không rõ ràng
}

6. Null vs Missing Fields

// Option 1: Include null values
{
  "name": "John",
  "phone": null,      // Explicitly null
  "address": null
}

// Option 2: Omit null values
{
  "name": "John"
  // phone và address không có trong response
}

// Chọn 1 approach và giữ consistent!

7. Pagination Best Practices

{
  "data": [...],
  "pagination": {
    "page": 2,
    "per_page": 20,
    "total": 157,
    "total_pages": 8,
    "has_next": true,
    "has_prev": true
  },
  "links": {
    "self": "/api/users?page=2",
    "first": "/api/users?page=1",
    "prev": "/api/users?page=1",
    "next": "/api/users?page=3",
    "last": "/api/users?page=8"
  }
}

8. Security Best Practices

Không expose sensitive data

// ✗ Bad: Expose internal errors
{
  "error": "Database error",
  "message": "SELECT * FROM users WHERE id = '1; DROP TABLE users;--'"
}

// ✓ Good: Generic message, log internally
{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "request_id": "abc123"  // Dùng để trace trong logs
}

// ✗ Bad: Return password/tokens
{
  "id": 1,
  "email": "user@example.com",
  "password_hash": "$2b$10$...",  // NEVER!
  "api_key": "sk_live_..."        // NEVER!
}

// ✓ Good: Only return safe fields
{
  "id": 1,
  "email": "user@example.com",
  "name": "John Doe"
}

Issue 1: JSON Parse Error

Problem: Response không phải valid JSON

// Server trả về HTML thay vì JSON (ví dụ: error page)
<html><body>Internal Server Error</body></html>

Solution:

test('Handle non-JSON response', async ({ request }) => {
  const response = await request.get('https://api.example.com/endpoint');

  // Check content-type trước
  const contentType = response.headers()['content-type'];

  if (contentType?.includes('application/json')) {
    const json = await response.json();
    // Process JSON
  } else {
    const text = await response.text();
    console.log('Non-JSON response:', text);
    // Handle error page, etc.
  }
});

Issue 2: Empty Response Body

Problem: Gọi .json() trên empty body

// 204 No Content không có body
const response = await request.delete('/users/1');
const body = await response.json(); // Error!

Solution:

test('Handle empty body', async ({ request }) => {
  const response = await request.delete('https://api.example.com/users/1');

  if (response.status() === 204) {
    // No body expected
    expect(await response.text()).toBe('');
  } else {
    const body = await response.json();
    // Process body
  }
});

Issue 3: Large Response Body

Problem: Response quá lớn, timeout hoặc memory issues

Solution:

test('Handle large response', async ({ request }) => {
  // Use pagination
  const response = await request.get('https://api.example.com/users', {
    params: {
      page: 1,
      limit: 100  // Limit items per request
    }
  });

  const result = await response.json();

  // Stream for very large files
  // (Playwright không hỗ trợ streaming, cần dùng alternatives)
});

Issue 4: Encoding Issues

Problem: Unicode/UTF-8 characters bị corrupt

// Tiếng Việt bị lỗi
{"name": "Nguy\u00e1\u00bb\u0085n V\u00c4\u0083n A"}

Solution:

// Server cần set đúng charset
Content-Type: application/json; charset=utf-8

// Verify trong test
test('Handle Vietnamese characters', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  const user = await response.json();

  // Should be properly decoded
  expect(user.name).toBe('Nguyễn Văn A');
});

Issue 5: Inconsistent Data Types

Problem: Cùng field có different types

// Lúc là string, lúc là number
{"id": 1}      // Number
{"id": "1"}    // String

// Lúc là array, lúc là null
{"items": []}
{"items": null}

Solution:

test('Handle inconsistent types', async ({ request }) => {
  const response = await request.get('https://api.example.com/data');
  const data = await response.json();

  // Normalize id to number
  const id = typeof data.id === 'string' ? parseInt(data.id) : data.id;

  // Handle null/undefined arrays
  const items = data.items || [];

  expect(typeof id).toBe('number');
  expect(Array.isArray(items)).toBeTruthy();
});

Issue 6: Nested Error Details

Problem: Error details nằm sâu trong response

{
  "response": {
    "error": {
      "details": {
        "message": "The actual error message"
      }
    }
  }
}

Solution:

test('Extract nested error', async ({ request }) => {
  const response = await request.post('https://api.example.com/endpoint', {
    data: { invalid: 'data' }
  });

  const body = await response.json();

  // Navigate nested structure safely
  const errorMessage = body?.response?.error?.details?.message
    || body?.error?.message
    || body?.message
    || 'Unknown error';

  expect(errorMessage).toBeDefined();
});

Complete Error Handling Example

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

async function parseResponse(response) {
  const contentType = response.headers()['content-type'] || '';

  // Empty body check
  const text = await response.text();
  if (!text) {
    return { empty: true, status: response.status() };
  }

  // JSON response
  if (contentType.includes('application/json')) {
    try {
      return JSON.parse(text);
    } catch (e) {
      return { parseError: true, raw: text };
    }
  }

  // Non-JSON response
  return { raw: text, contentType };
}

test('Robust response handling', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  const result = await parseResponse(response);

  if (result.empty) {
    console.log('Empty response with status:', result.status);
    return;
  }

  if (result.parseError) {
    console.log('Failed to parse JSON:', result.raw);
    return;
  }

  if (result.raw) {
    console.log('Non-JSON response:', result.raw);
    return;
  }

  // Valid JSON response
  expect(result).toHaveProperty('id');
  expect(result).toHaveProperty('name');
});