← Trở về trang chủ

Query Parameters

Định nghĩa

Query Parameters (hay Query String) là cách gửi dữ liệu kèm theo URL, xuất hiện sau dấu ? trong URL.

Cấu trúc URL với Query Parameters

https://api.example.com/users?age=25&city=hanoi&status=active

Phân tích:

  • https://api.example.com/users - Base URL
  • ? - Dấu bắt đầu query string
  • age=25 - Parameter đầu tiên (key=value)
  • & - Dấu phân tách giữa các parameters
  • city=hanoi - Parameter thứ hai
  • status=active - Parameter thứ ba

Cú pháp cơ bản

// Single parameter
?key=value

// Multiple parameters
?key1=value1&key2=value2&key3=value3

// Ví dụ thực tế
?name=john&age=30&city=hanoi

Khi nào sử dụng Query Parameters?

  • GET requests: Lấy dữ liệu với filtering/sorting
  • Optional parameters: Các tham số không bắt buộc
  • Filtering: Lọc kết quả (?status=active)
  • Sorting: Sắp xếp (?sort=name)
  • Pagination: Phân trang (?page=2&limit=20)
  • Search: Tìm kiếm (?q=keyword)
  • Shareable URLs: URL có thể bookmark/share

Khi KHÔNG nên dùng Query Parameters

  • Sensitive data: Passwords, tokens, API keys
  • Large data: Dữ liệu lớn (dùng request body)
  • Required parameters: Tham số bắt buộc (dùng path parameters)
  • POST/PUT/DELETE: Nên dùng request body
  • Binary data: Files, images (dùng multipart form)

Ví dụ thực tế

// 1. Lọc sản phẩm theo category và giá
GET /api/products?category=laptop&price_min=10000000&price_max=20000000

// 2. Tìm kiếm users theo tên
GET /api/users?search=nguyen&role=admin

// 3. Phân trang
GET /api/posts?page=3&limit=20&sort=created_at&order=desc

// 4. Lấy thông tin thời tiết theo tọa độ
GET /api/weather?lat=21.0285&lon=105.8542&units=metric

// 5. Multiple filters
GET /api/orders?status=completed&date_from=2025-01-01&date_to=2025-01-31&customer_id=123

Ví dụ với Playwright

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

test('GET với query parameters', async ({ request }) => {
  // Cách 1: Viết trực tiếp trong URL
  const response1 = await request.get(
    'https://api.example.com/users?age=25&city=hanoi'
  );

  // Cách 2: Dùng params object (recommended)
  const response2 = await request.get('https://api.example.com/users', {
    params: {
      age: 25,
      city: 'hanoi',
      status: 'active'
    }
  });

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

  const users = await response2.json();
  expect(users).toBeInstanceOf(Array);
});

Single Parameter

?name=john

Multiple Parameters

?name=john&age=30&city=hanoi

Array Values

Có nhiều cách để gửi array values, không có chuẩn chung:

// 1. PHP style (phổ biến nhất)
?tags[]=php&tags[]=api&tags[]=testing

// 2. Repeat key
?tags=php&tags=api&tags=testing

// 3. Comma-separated
?tags=php,api,testing

// 4. Brackets with index
?tags[0]=php&tags[1]=api&tags[2]=testing

Test array parameters với Playwright

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

test('Array parameters - different approaches', async ({ request }) => {
  // Approach 1: Comma-separated (simplest)
  const response1 = await request.get('https://api.example.com/posts', {
    params: {
      tags: 'php,api,testing'
    }
  });
  // URL: /posts?tags=php,api,testing

  // Approach 2: Array (Playwright tự động convert)
  const response2 = await request.get('https://api.example.com/posts', {
    params: {
      tags: ['php', 'api', 'testing']
    }
  });
  // URL: /posts?tags=php&tags=api&tags=testing

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

Nested Objects

Gửi object lồng nhau:

// Filter object
?filter[name]=john&filter[age]=30&filter[city]=hanoi

// Sort object
?sort[field]=created_at&sort[order]=desc

// Range object
?price[min]=100&price[max]=500

Nested parameters với Playwright

test('Nested object parameters', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      'filter[category]': 'laptop',
      'filter[brand]': 'dell',
      'price[min]': 10000000,
      'price[max]': 20000000,
      'sort[field]': 'price',
      'sort[order]': 'asc'
    }
  });

  // URL: /products?filter[category]=laptop&filter[brand]=dell&price[min]=10000000...

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

Special Characters & URL Encoding

Một số ký tự đặc biệt cần được encode:

Ký tự URL Encoded Ví dụ
Space %20 hoặc + hello world → hello%20world
@ %40 user@example.com → user%40example.com
& %26 A&B → A%26B
= %3D x=y → x%3Dy
? %3F what? → what%3F
# %23 #hashtag → %23hashtag
/ %2F path/to → path%2Fto
Tiếng Việt UTF-8 encoded Hà Nội → H%C3%A0%20N%E1%BB%99i

URL Encoding trong thực tế

// ✗ Sai: Không encode
?email=user@example.com&name=Nguyễn Văn A&search=hello world

// ✓ Đúng: Encoded
?email=user%40example.com&name=Nguy%E1%BB%85n%20V%C4%83n%20A&search=hello%20world

// Playwright tự động encode
const response = await request.get('https://api.example.com/users', {
  params: {
    email: 'user@example.com',    // Tự động encode thành user%40example.com
    name: 'Nguyễn Văn A',         // Tự động encode UTF-8
    search: 'hello world'         // Tự động encode spaces
  }
});

Empty Values

Sự khác biệt giữa empty string và undefined:

// 1. Parameter có value rỗng
?search=
// Có parameter "search" với value = ""

// 2. Parameter không có value
?search
// Có parameter "search" nhưng không có value

// 3. Parameter không tồn tại
(không có trong URL)
// Không có parameter "search"

Handle empty values

test('Empty values handling', async ({ request }) => {
  // Empty string
  const response1 = await request.get('https://api.example.com/search', {
    params: {
      q: '',           // search?q=
      category: 'tech' // search?q=&category=tech
    }
  });

  // Undefined (parameter bị bỏ qua)
  const response2 = await request.get('https://api.example.com/search', {
    params: {
      q: undefined,    // Không xuất hiện trong URL
      category: 'tech' // search?category=tech
    }
  });

  // Null (tùy library có thể khác nhau)
  const response3 = await request.get('https://api.example.com/search', {
    params: {
      q: null,         // Có thể thành search?q= hoặc bị bỏ qua
      category: 'tech'
    }
  });
});

Boolean Values

// String representation
?is_active=true
?verified=false

// Numeric representation (0/1)
?is_active=1
?verified=0

// Presence check (có parameter = true)
?is_active
// Không có parameter = false
⚠️ Lưu ý:
  • Query parameters luôn là strings khi đến server
  • Server cần convert sang đúng type (number, boolean, ...)
  • ?age=25 → Server nhận "25" (string), cần parse thành number
  • ?active=true → Server nhận "true" (string), cần convert thành boolean

Basic Filtering

// Filter by status
GET /api/orders?status=completed

// Filter by category
GET /api/products?category=laptop

// Filter by boolean
GET /api/users?is_active=true&verified=true

Multiple Filters

// Combine multiple conditions (AND)
GET /api/products?category=laptop&brand=dell&in_stock=true

// Price range
GET /api/products?price_min=10000000&price_max=20000000

// Date range
GET /api/orders?date_from=2025-01-01&date_to=2025-01-31

Complex Filtering

// Advanced filters với operators
GET /api/users?age[gte]=18&age[lte]=65
// age >= 18 AND age <= 65

GET /api/products?price[gt]=1000000&rating[gte]=4
// price > 1000000 AND rating >= 4

GET /api/posts?status[in]=published,draft
// status IN ('published', 'draft')

GET /api/users?name[like]=%nguyen%
// name LIKE '%nguyen%'

Ví dụ Filtering với Playwright

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

test('Basic filtering', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      category: 'laptop',
      brand: 'dell',
      in_stock: true
    }
  });

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

  const products = await response.json();

  // Verify tất cả products đều match filters
  products.forEach(product => {
    expect(product.category).toBe('laptop');
    expect(product.brand).toBe('dell');
    expect(product.in_stock).toBe(true);
  });
});

test('Price range filtering', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      price_min: 10000000,
      price_max: 20000000
    }
  });

  const products = await response.json();

  // Verify tất cả products trong price range
  products.forEach(product => {
    expect(product.price).toBeGreaterThanOrEqual(10000000);
    expect(product.price).toBeLessThanOrEqual(20000000);
  });
});

test('Date range filtering', async ({ request }) => {
  const response = await request.get('https://api.example.com/orders', {
    params: {
      date_from: '2025-01-01',
      date_to: '2025-01-31',
      status: 'completed'
    }
  });

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

  const orders = await response.json();
  expect(orders).toBeInstanceOf(Array);
});

Common Filter Operators

Operator Ý nghĩa Ví dụ
eq Bằng (equal) ?age[eq]=25
ne Không bằng (not equal) ?status[ne]=deleted
gt Lớn hơn (greater than) ?price[gt]=1000000
gte Lớn hơn hoặc bằng ?age[gte]=18
lt Nhỏ hơn (less than) ?quantity[lt]=10
lte Nhỏ hơn hoặc bằng ?age[lte]=65
in Trong danh sách ?status[in]=active,pending
nin Không trong danh sách ?role[nin]=admin,superadmin
like Pattern matching ?name[like]=%nguyen%
between Trong khoảng ?price[between]=100,500

Basic Sorting

// Sort by name (ascending - mặc định)
GET /api/users?sort=name

// Sort by created date (descending)
GET /api/posts?sort=-created_at
// Dấu "-" = descending

// Sort by multiple fields
GET /api/products?sort=category,price
// Sort by category first, then by price

Explicit Order Direction

// Style 1: Separate order parameter
GET /api/users?sort=name&order=asc
GET /api/users?sort=created_at&order=desc

// Style 2: orderBy và direction
GET /api/products?orderBy=price&direction=asc

// Style 3: sort object
GET /api/posts?sort[field]=created_at&sort[order]=desc

Multiple Sort Fields

// Comma-separated
GET /api/products?sort=category,-price,name
// Sort by: category ASC, price DESC, name ASC

// Array style
GET /api/users?sort[]=created_at:desc&sort[]=name:asc

// Object style
GET /api/posts?sort[0][field]=published_at&sort[0][order]=desc&sort[1][field]=title&sort[1][order]=asc

Ví dụ Sorting với Playwright

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

test('Basic sorting - ascending', async ({ request }) => {
  const response = await request.get('https://api.example.com/users', {
    params: {
      sort: 'name',
      order: 'asc'
    }
  });

  const users = await response.json();

  // Verify sorted correctly
  for (let i = 0; i < users.length - 1; i++) {
    expect(users[i].name <= users[i + 1].name).toBeTruthy();
  }
});

test('Sorting - descending by date', async ({ request }) => {
  const response = await request.get('https://api.example.com/posts', {
    params: {
      sort: 'created_at',
      order: 'desc'
    }
  });

  const posts = await response.json();

  // Verify newest first
  for (let i = 0; i < posts.length - 1; i++) {
    const date1 = new Date(posts[i].created_at);
    const date2 = new Date(posts[i + 1].created_at);
    expect(date1 >= date2).toBeTruthy();
  }
});

test('Multiple sort fields', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      sort: 'category,-price'
      // Sort by category ASC, then price DESC
    }
  });

  const products = await response.json();
  expect(products.length).toBeGreaterThan(0);
});

test('Combine filter and sort', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      category: 'laptop',
      in_stock: true,
      sort: 'price',
      order: 'asc'
    }
  });

  const products = await response.json();

  // Verify filtered
  products.forEach(p => {
    expect(p.category).toBe('laptop');
    expect(p.in_stock).toBe(true);
  });

  // Verify sorted
  for (let i = 0; i < products.length - 1; i++) {
    expect(products[i].price <= products[i + 1].price).toBeTruthy();
  }
});

Common Sort Patterns

Pattern Ví dụ Framework
Prefix - ?sort=-created_at JSON:API, Rails
Separate order ?sort=name&order=desc Common
orderBy/direction ?orderBy=price&direction=asc Laravel
Colon separator ?sort=name:asc Custom
Object notation ?sort[field]=name&sort[order]=asc Custom

Page-based Pagination

// Page và limit
GET /api/users?page=1&limit=20
GET /api/users?page=2&limit=20

// Page và per_page (Rails style)
GET /api/posts?page=3&per_page=50

// Page bắt đầu từ 0
GET /api/products?page=0&size=25

Offset-based Pagination

// Offset và limit
GET /api/users?offset=0&limit=20   // Records 1-20
GET /api/users?offset=20&limit=20  // Records 21-40
GET /api/users?offset=40&limit=20  // Records 41-60

// Skip và take
GET /api/products?skip=0&take=50

Cursor-based Pagination

// First page
GET /api/posts?limit=20

// Response includes cursor
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTAwfQ=="
}

// Next page
GET /api/posts?limit=20&cursor=eyJpZCI6MTAwfQ==

Ví dụ Pagination với Playwright

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

test('Page-based pagination', async ({ request }) => {
  const limit = 20;

  // Page 1
  const page1 = await request.get('https://api.example.com/users', {
    params: { page: 1, limit }
  });
  const data1 = await page1.json();

  expect(data1.data.length).toBeLessThanOrEqual(limit);
  expect(data1.page).toBe(1);
  expect(data1.total).toBeGreaterThan(0);

  // Page 2
  const page2 = await request.get('https://api.example.com/users', {
    params: { page: 2, limit }
  });
  const data2 = await page2.json();

  // Verify khác nhau
  expect(data1.data[0].id).not.toBe(data2.data[0].id);
});

test('Offset-based pagination', async ({ request }) => {
  const limit = 10;

  // Get first 10
  const response1 = await request.get('https://api.example.com/products', {
    params: { offset: 0, limit }
  });
  const data1 = await response1.json();

  // Get next 10
  const response2 = await request.get('https://api.example.com/products', {
    params: { offset: 10, limit }
  });
  const data2 = await response2.json();

  expect(data1.length).toBe(limit);
  expect(data2.length).toBe(limit);

  // Verify không overlap
  expect(data1[data1.length - 1].id).not.toBe(data2[0].id);
});

test('Load all pages', async ({ request }) => {
  const limit = 20;
  let page = 1;
  let allUsers = [];

  while (true) {
    const response = await request.get('https://api.example.com/users', {
      params: { page, limit }
    });

    const data = await response.json();
    allUsers = allUsers.concat(data.data);

    console.log(`Loaded page ${page}, total users: ${allUsers.length}`);

    // Nếu không còn data hoặc đã đến trang cuối
    if (data.data.length < limit || !data.has_more) {
      break;
    }

    page++;
  }

  console.log(`Total users loaded: ${allUsers.length}`);
  expect(allUsers.length).toBeGreaterThan(0);
});

So sánh các kiểu Pagination

Type Ưu điểm Nhược điểm Use case
Page-based Đơn giản, dễ hiểu, có thể jump tới page bất kỳ Kém performance với data lớn, vấn đề với data thay đổi Admin panels, danh sách thông thường
Offset-based Linh hoạt, có thể skip số lượng bất kỳ Chậm với offset lớn, không consistent khi data thay đổi Infinite scroll với số lượng giới hạn
Cursor-based Performance tốt, consistent khi data thay đổi Phức tạp, không thể jump tới page giữa Social feeds, real-time data, infinite scroll

Pagination Response Format

// Standard pagination response
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 157,
    "total_pages": 8,
    "has_next": true,
    "has_prev": true
  }
}

// With links
{
  "data": [...],
  "links": {
    "first": "/api/users?page=1&limit=20",
    "prev": "/api/users?page=1&limit=20",
    "next": "/api/users?page=3&limit=20",
    "last": "/api/users?page=8&limit=20"
  },
  "meta": {
    "current_page": 2,
    "per_page": 20,
    "total": 157
  }
}
⚠️ Best Practices:
  • Set default limit (ví dụ: 20 hoặc 50)
  • Set maximum limit để tránh quá tải (ví dụ: max 100)
  • Trả về total count để client biết có bao nhiêu trang
  • Include has_next/has_prev để client biết có thể load thêm không
  • Validate page/offset values (không âm, không quá lớn)

Basic Search

// Single query parameter
GET /api/users?q=john
GET /api/products?search=laptop
GET /api/posts?query=api+testing

Search trong fields cụ thể

// Search in specific fields
GET /api/users?search=nguyen&fields=name,email

// Separate parameters cho từng field
GET /api/users?name=nguyen&email=gmail.com

// Advanced search
GET /api/products?name[like]=%laptop%&description[like]=%gaming%

Full-text Search

// Simple full-text
GET /api/posts?q=api+testing+tutorial

// With search mode
GET /api/articles?q=playwright&mode=fulltext

// With relevance
GET /api/search?q=javascript&min_score=0.5

Ví dụ Search với Playwright

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

test('Basic search', async ({ request }) => {
  const response = await request.get('https://api.example.com/users', {
    params: {
      q: 'nguyen'
    }
  });

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

  const users = await response.json();

  // Verify kết quả chứa search term
  users.forEach(user => {
    const matchesName = user.name.toLowerCase().includes('nguyen');
    const matchesEmail = user.email.toLowerCase().includes('nguyen');
    expect(matchesName || matchesEmail).toBeTruthy();
  });
});

test('Search with filters', async ({ request }) => {
  const response = await request.get('https://api.example.com/products', {
    params: {
      search: 'laptop',
      category: 'electronics',
      price_max: 20000000,
      in_stock: true
    }
  });

  const products = await response.json();

  // Verify search + filters
  products.forEach(product => {
    expect(product.name.toLowerCase()).toContain('laptop');
    expect(product.category).toBe('electronics');
    expect(product.price).toBeLessThanOrEqual(20000000);
    expect(product.in_stock).toBe(true);
  });
});

test('Search with pagination and sorting', async ({ request }) => {
  const response = await request.get('https://api.example.com/posts', {
    params: {
      q: 'api testing',
      page: 1,
      limit: 20,
      sort: 'relevance',  // Hoặc 'created_at', 'title'
      order: 'desc'
    }
  });

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

  const data = await response.json();
  expect(data.results).toBeInstanceOf(Array);
  expect(data.results.length).toBeLessThanOrEqual(20);
});

Search với Special Characters

// Search có spaces
GET /api/posts?q=api+testing
GET /api/posts?q=api%20testing

// Search có quotes
GET /api/search?q="exact phrase"
GET /api/search?q=%22exact%20phrase%22

// Search có special characters
GET /api/users?email=user%40example.com  // @ được encode
GET /api/search?q=C%23+programming       // # được encode

Search Best Practices

  • ✓ Hỗ trợ case-insensitive search
  • ✓ Trim whitespace ở đầu/cuối search term
  • ✓ Có minimum search length (ví dụ: >= 3 ký tự)
  • ✓ Return relevance score nếu có
  • ✓ Hỗ trợ wildcards (* hoặc %)
  • ✓ Escape special characters để tránh SQL injection
  • ✓ Combine search với filters, sorting, pagination

Select specific fields

// Only return id, name, email
GET /api/users?fields=id,name,email

// Exclude password and token
GET /api/users?exclude=password,token

// Include related data
GET /api/posts?include=author,comments

Nested field selection

// Select fields from related objects
GET /api/posts?fields=title,content&include=author&author_fields=name,email

// Sparse fieldsets (JSON:API style)
GET /api/articles?fields[articles]=title,body&fields[people]=name

Ví dụ Field Selection

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

test('Select specific fields', async ({ request }) => {
  const response = await request.get('https://api.example.com/users', {
    params: {
      fields: 'id,name,email'
    }
  });

  const users = await response.json();

  // Verify chỉ có fields đã chọn
  users.forEach(user => {
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('email');

    // Không có fields khác
    expect(user).not.toHaveProperty('password');
    expect(user).not.toHaveProperty('created_at');
  });
});

test('Include related data', async ({ request }) => {
  const response = await request.get('https://api.example.com/posts/1', {
    params: {
      include: 'author,comments'
    }
  });

  const post = await response.json();

  expect(post).toHaveProperty('id');
  expect(post).toHaveProperty('title');
  expect(post).toHaveProperty('author');      // Included
  expect(post).toHaveProperty('comments');    // Included
  expect(post.author).toHaveProperty('name');
  expect(post.comments).toBeInstanceOf(Array);
});

Benefits của Field Selection

  • Reduce bandwidth: Chỉ trả về data cần thiết
  • Faster response: Less data to serialize
  • Privacy: Tránh expose sensitive fields
  • Flexibility: Client control response structure
  • Mobile-friendly: Giảm data transfer cho mobile apps

Date Range Parameters

// From and to
GET /api/orders?date_from=2025-01-01&date_to=2025-01-31

// Start and end
GET /api/logs?start_date=2025-01-01&end_date=2025-12-31

// After and before
GET /api/posts?created_after=2025-01-01&created_before=2025-02-01

// Specific field
GET /api/orders?created_at[gte]=2025-01-01&created_at[lte]=2025-01-31

Date Formats

// ISO 8601 date (recommended)
?date=2025-01-15

// ISO 8601 datetime
?created_at=2025-01-15T10:30:00Z

// Unix timestamp
?timestamp=1736935800

// Relative dates
?date=today
?date=yesterday
?date=last_7_days
?date=this_month

Ví dụ Date Range với Playwright

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

test('Date range filtering', async ({ request }) => {
  const response = await request.get('https://api.example.com/orders', {
    params: {
      date_from: '2025-01-01',
      date_to: '2025-01-31'
    }
  });

  const orders = await response.json();

  // Verify tất cả orders trong date range
  orders.forEach(order => {
    const orderDate = new Date(order.created_at);
    expect(orderDate >= new Date('2025-01-01')).toBeTruthy();
    expect(orderDate <= new Date('2025-01-31')).toBeTruthy();
  });
});

test('Get last 7 days data', async ({ request }) => {
  const today = new Date();
  const last7Days = new Date();
  last7Days.setDate(today.getDate() - 7);

  const response = await request.get('https://api.example.com/analytics', {
    params: {
      start_date: last7Days.toISOString().split('T')[0],
      end_date: today.toISOString().split('T')[0]
    }
  });

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

test('Filter by datetime with timezone', async ({ request }) => {
  const response = await request.get('https://api.example.com/events', {
    params: {
      start: '2025-01-15T00:00:00Z',
      end: '2025-01-15T23:59:59Z'
    }
  });

  const events = await response.json();
  expect(events).toBeInstanceOf(Array);
});
⚠️ Date/Time Best Practices:
  • Dùng ISO 8601 format: YYYY-MM-DD hoặc YYYY-MM-DDTHH:mm:ssZ
  • Luôn specify timezone (UTC recommended)
  • Validate date ranges (from <= to)
  • Set max range để tránh query quá nhiều data
  • Handle inclusive vs exclusive ranges rõ ràng

Query Parameters vs Path Parameters

Aspect Query Parameters Path Parameters
Syntax /users?id=123 /users/123
Required Optional Required (part of route)
Use case Filtering, sorting, options Resource identification
Multiple values ?tags=php&tags=api Khó (cần nhiều segments)
SEO Kém hơn Tốt hơn

Ví dụ so sánh

// ✓ Path param: Identify specific resource
GET /api/users/123              // Get user with ID 123
GET /api/posts/456/comments/789 // Get comment 789 of post 456

// ✓ Query param: Optional filtering/options
GET /api/users?role=admin&active=true
GET /api/products?category=laptop&sort=price

// ✗ Không nên: Required param trong query
GET /api/users?id=123  // Nên dùng /users/123

// ✗ Không nên: Complex path
GET /api/filter/category/laptop/price/1000000/2000000  // Quá dài!

Query Parameters vs Request Body

Aspect Query Parameters Request Body
HTTP Methods GET, DELETE POST, PUT, PATCH
Size limit ~2KB (URL length limit) Lớn hơn nhiều (MB+)
Cacheable Yes No (thường)
Visible in URL Yes No
Bookmarkable Yes No
Security Logged, visible More secure

Khi nào dùng gì?

// ✓ Query params: GET với simple filters
GET /api/products?category=laptop&price_max=20000000

// ✓ Body: POST/PUT với complex data
POST /api/users
Body: {
  "name": "Nguyễn Văn A",
  "email": "vana@example.com",
  "address": {
    "street": "123 Main St",
    "city": "Hanoi"
  }
}

// ✓ Body: Complex search với many conditions
POST /api/search
Body: {
  "filters": [
    {"field": "category", "op": "eq", "value": "laptop"},
    {"field": "price", "op": "between", "value": [10000000, 20000000]},
    {"field": "brand", "op": "in", "value": ["dell", "hp", "lenovo"]}
  ],
  "sort": [
    {"field": "price", "order": "asc"},
    {"field": "rating", "order": "desc"}
  ],
  "page": 1,
  "limit": 20
}

// ✗ Không nên: Sensitive data trong query
GET /api/login?email=user@example.com&password=123456  // Nguy hiểm!

// ✓ Đúng: Dùng POST body
POST /api/login
Body: {
  "email": "user@example.com",
  "password": "123456"
}

Query Parameters vs Headers

Use for Query Parameters Headers
Filtering data
Authentication
Content negotiation
Pagination
API versioning Both OK Both OK

Decision Tree: Chọn phương pháp nào?

Is it required to identify the resource?
  → YES: Use PATH parameter (/users/123)
  → NO: Continue...

Is it sensitive data (password, token)?
  → YES: Use REQUEST BODY (POST) or HEADER (Authorization)
  → NO: Continue...

Is it large/complex data?
  → YES: Use REQUEST BODY
  → NO: Continue...

Is it metadata about the request?
  → YES: Use HEADER (Accept, Content-Type, ...)
  → NO: Continue...

Is it filtering/sorting/pagination?
  → YES: Use QUERY PARAMETERS
  → DEFAULT: Use QUERY PARAMETERS

Naming Conventions

snake_case (Recommended)

?first_name=john&last_name=doe
?created_at=2025-01-01
?is_active=true
?price_min=100&price_max=500

camelCase

?firstName=john&lastName=doe
?createdAt=2025-01-01
?isActive=true
?priceMin=100&priceMax=500

Consistency là quan trọng nhất!

// ✓ Consistent (tất cả snake_case)
?first_name=john&last_name=doe&is_active=true

// ✗ Inconsistent (mixed)
?firstName=john&last_name=doe&isActive=true

Security Best Practices

1. Validate và Sanitize Input

// ✗ Nguy hiểm: SQL Injection
const userId = req.query.id;
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// ✓ An toàn: Parameterized query
const userId = parseInt(req.query.id);
db.query('SELECT * FROM users WHERE id = ?', [userId]);

2. Không gửi Sensitive Data

// ✗ Nguy hiểm
GET /api/reset-password?email=user@example.com&new_password=123456

// ✓ An toàn
POST /api/reset-password
Body: {
  "email": "user@example.com",
  "new_password": "123456"
}

3. XSS Protection

// ✗ Nguy hiểm
const search = req.query.q;
res.send(`<h1>Search results for: ${search}</h1>`);
// Nếu q = "<script>alert('xss')</script>"

// ✓ An toàn: Escape HTML
const search = escapeHtml(req.query.q);
res.send(`<h1>Search results for: ${search}</h1>`);

4. Rate Limiting

// Prevent abuse của search/filter endpoints
GET /api/search?q=a  // Quá ngắn
GET /api/search?q=aaaaaaa...  // Quá dài
GET /api/users?limit=999999   // Quá nhiều

// Server nên:
// - Minimum search length (>= 2-3 chars)
// - Maximum search length (<= 100 chars)
// - Maximum limit (<= 100)
// - Rate limit (max X requests per minute)

Performance Best Practices

1. Set Default Values

// Server code example
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const sort = req.query.sort || 'created_at';
const order = req.query.order || 'desc';

2. Validate Ranges

// Validate pagination
if (page < 1) page = 1;
if (limit < 1) limit = 20;
if (limit > 100) limit = 100;  // Max limit

// Validate date range
if (dateFrom > dateTo) {
  return error("date_from must be before date_to");
}

const maxDays = 365;
if (dateTo - dateFrom > maxDays * 24 * 60 * 60 * 1000) {
  return error("Date range cannot exceed 1 year");
}

3. Cache-friendly Parameters

// ✓ Good: Consistent parameter order (cache-friendly)
/api/products?category=laptop&sort=price&page=1
/api/products?category=laptop&sort=price&page=2

// ✗ Bad: Random order (different cache keys)
/api/products?sort=price&category=laptop&page=1
/api/products?page=1&category=laptop&sort=price

User Experience Best Practices

1. Clear Parameter Names

// ✓ Clear
?search=laptop
?category=electronics
?price_min=1000000
?sort=price&order=asc

// ✗ Unclear
?s=laptop
?cat=e
?pmin=1000000
?sb=p&so=a

2. Reasonable Defaults

// Default values make sense
GET /api/products
// Defaults: page=1, limit=20, sort=created_at, order=desc

GET /api/users
// Defaults: active=true, page=1, limit=50

3. Error Messages

// ✓ Good error message
{
  "error": "Invalid parameter",
  "message": "Parameter 'limit' must be between 1 and 100. You provided: 999",
  "field": "limit",
  "provided_value": 999,
  "valid_range": [1, 100]
}

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

Documentation Best Practices

Nên document rõ ràng:

  • ✓ Parameter name và type
  • ✓ Required hay optional
  • ✓ Default value
  • ✓ Valid values / range
  • ✓ Examples
/**
 * GET /api/products
 *
 * Query Parameters:
 * - category (string, optional): Product category
 *   Example: "laptop", "phone"
 *
 * - price_min (number, optional): Minimum price
 *   Range: 0 - 999999999
 *   Default: 0
 *
 * - price_max (number, optional): Maximum price
 *   Range: 0 - 999999999
 *   Default: 999999999
 *
 * - sort (string, optional): Sort field
 *   Valid values: "price", "name", "created_at", "rating"
 *   Default: "created_at"
 *
 * - order (string, optional): Sort order
 *   Valid values: "asc", "desc"
 *   Default: "desc"
 *
 * - page (number, optional): Page number
 *   Range: 1 - 9999
 *   Default: 1
 *
 * - limit (number, optional): Items per page
 *   Range: 1 - 100
 *   Default: 20
 *
 * Example:
 * GET /api/products?category=laptop&price_max=20000000&sort=price&order=asc&page=1&limit=20
 */

Issue 1: URL Length Limits

Problem: URLs quá dài bị reject hoặc truncate

// URL quá dài (~10KB)
GET /api/products?id=1&id=2&id=3&id=4&id=5&...&id=1000

Limits:

  • Browsers: ~2000 characters (IE), ~65000 (Chrome/Firefox)
  • Web servers: Configurable (default ~8KB)
  • CDNs/Proxies: Varies

Solutions:

// ✓ Solution 1: Dùng POST với body
POST /api/products/bulk-get
Body: {
  "ids": [1, 2, 3, ..., 1000]
}

// ✓ Solution 2: Pagination
GET /api/products?page=1&limit=100
GET /api/products?page=2&limit=100

// ✓ Solution 3: Shorten parameter names
?ids=1,2,3,4,5  // Thay vì ?id=1&id=2&id=3...

Issue 2: Special Characters Encoding

Problem: Special characters không được encode đúng

// ✗ Broken
GET /api/search?q=A&B  // "&B" bị hiểu là parameter khác!
GET /api/users?email=user@example.com  // "@" có thể gây lỗi

Solution:

// ✓ Encode properly
GET /api/search?q=A%26B  // %26 = &
GET /api/users?email=user%40example.com  // %40 = @

// ✓ Playwright auto-encodes
const response = await request.get('/api/search', {
  params: {
    q: 'A&B',  // Tự động encode thành A%26B
    email: 'user@example.com'  // Tự động encode
  }
});

Issue 3: Array Parameter Handling

Problem: Không có chuẩn chung cho array parameters

Các format khác nhau:

// PHP style
?tags[]=php&tags[]=api

// Rails style / Standard
?tags=php&tags=api

// Comma-separated
?tags=php,api

// JSON
?tags=["php","api"]  // Cần encode

Solution: Document rõ ràng format nào API hỗ trợ

// Example documentation
/**
 * tags parameter accepts multiple values in these formats:
 * 1. Comma-separated: ?tags=php,api,testing (recommended)
 * 2. Repeated keys: ?tags=php&tags=api&tags=testing
 */

Issue 4: Type Conversion

Problem: Query params luôn là strings, cần convert

// Client gửi
?age=25&active=true&price=10.5

// Server nhận (tất cả là strings!)
{
  age: "25",        // String, không phải number!
  active: "true",   // String, không phải boolean!
  price: "10.5"     // String, không phải number!
}

Solution: Parse và validate

// Server-side parsing
const age = parseInt(req.query.age);
const active = req.query.active === 'true';
const price = parseFloat(req.query.price);

// With validation
const age = parseInt(req.query.age);
if (isNaN(age) || age < 0 || age > 150) {
  return error("Invalid age");
}

// Test code
test('Type handling', async ({ request }) => {
  const response = await request.get('/api/users', {
    params: {
      age: 25,        // Number
      active: true,   // Boolean
      price: 10.5     // Float
    }
  });

  // Playwright converts to strings in URL:
  // /api/users?age=25&active=true&price=10.5

  // Server must parse back to correct types
});

Issue 5: Unicode và Tiếng Việt

Problem: Unicode characters (tiếng Việt, emoji, ...) không encode đúng

// ✗ Broken
?name=Nguyễn Văn A
?search=😀

Solution:

// ✓ URL encode UTF-8
?name=Nguy%E1%BB%85n%20V%C4%83n%20A
?search=%F0%9F%98%80

// ✓ Playwright handles automatically
const response = await request.get('/api/users', {
  params: {
    name: 'Nguyễn Văn A',  // Auto UTF-8 encode
    search: '😀'           // Auto encode emoji
  }
});

Issue 6: Parameter Pollution

Problem: Duplicate parameters với ý đồ xấu

// Attacker gửi
GET /api/transfer?amount=1&amount=99999

// Server chỉ lấy giá trị đầu (amount=1) để validate
// Nhưng lấy giá trị sau (amount=99999) để thực hiện
// → Bypass validation!

Solution:

// ✓ Reject duplicate params (nếu không hỗ trợ arrays)
if (req.query.amount && Array.isArray(req.query.amount)) {
  return error("Multiple 'amount' parameters not allowed");
}

// ✓ Hoặc chỉ lấy giá trị đầu tiên và validate
const amount = Array.isArray(req.query.amount)
  ? req.query.amount[0]
  : req.query.amount;

Complete Example: Robust Query Param Handling

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

test('Robust API with comprehensive validation', async ({ request }) => {
  // Test valid request
  const validResponse = await request.get('https://api.example.com/products', {
    params: {
      category: 'laptop',
      price_min: 10000000,
      price_max: 20000000,
      page: 1,
      limit: 20,
      sort: 'price',
      order: 'asc',
      search: 'Nguyễn gaming',  // UTF-8
      tags: 'dell,gaming'
    }
  });

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

  // Test invalid limit (quá lớn)
  const invalidLimit = await request.get('https://api.example.com/products', {
    params: {
      limit: 99999  // Server should reject or cap at max
    }
  });

  expect(invalidLimit.status()).toBe(400);
  const error1 = await invalidLimit.json();
  expect(error1.message).toContain('limit');

  // Test invalid price range
  const invalidRange = await request.get('https://api.example.com/products', {
    params: {
      price_min: 20000000,
      price_max: 10000000  // min > max!
    }
  });

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

  // Test SQL injection attempt
  const sqlInjection = await request.get('https://api.example.com/products', {
    params: {
      category: "' OR '1'='1"  // Should be escaped
    }
  });

  // Should either return empty results or error, NOT expose data
  expect(sqlInjection.status()).not.toBe(500);
});