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.
Phân tích:
? - Dấu bắt đầu query stringage=25 - Parameter đầu tiên (key=value)& - Dấu phân tách giữa các parameterscity=hanoi - Parameter thứ haistatus=active - Parameter thứ ba// Single parameter
?key=value
// Multiple parameters
?key1=value1&key2=value2&key3=value3
// Ví dụ thực tế
?name=john&age=30&city=hanoi
?status=active)?sort=name)?page=2&limit=20)?q=keyword)// 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
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);
});
?name=john
?name=john&age=30&city=hanoi
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
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);
});
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
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);
});
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 |
// ✗ 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
}
});
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"
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'
}
});
});
// 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
?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// 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
// 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
// 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%'
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);
});
| 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 |
// 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
// 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
// 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
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();
}
});
| 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 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 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
// First page
GET /api/posts?limit=20
// Response includes cursor
{
"data": [...],
"next_cursor": "eyJpZCI6MTAwfQ=="
}
// Next page
GET /api/posts?limit=20&cursor=eyJpZCI6MTAwfQ==
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);
});
| 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 |
// 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
}
}
// Single query parameter
GET /api/users?q=john
GET /api/products?search=laptop
GET /api/posts?query=api+testing
// 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%
// 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
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 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
// 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
// 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
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);
});
// 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
// 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
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);
});
| 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 |
// ✓ 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!
| 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 |
// ✓ 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"
}
| Use for | Query Parameters | Headers |
|---|---|---|
| Filtering data | ✓ | ✗ |
| Authentication | ✗ | ✓ |
| Content negotiation | ✗ | ✓ |
| Pagination | ✓ | ✗ |
| API versioning | Both OK | Both OK |
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
?first_name=john&last_name=doe
?created_at=2025-01-01
?is_active=true
?price_min=100&price_max=500
?firstName=john&lastName=doe
?createdAt=2025-01-01
?isActive=true
?priceMin=100&priceMax=500
// ✓ Consistent (tất cả snake_case)
?first_name=john&last_name=doe&is_active=true
// ✗ Inconsistent (mixed)
?firstName=john&last_name=doe&isActive=true
// ✗ 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]);
// ✗ 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"
}
// ✗ 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>`);
// 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)
// 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';
// 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");
}
// ✓ 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
// ✓ Clear
?search=laptop
?category=electronics
?price_min=1000000
?sort=price&order=asc
// ✗ Unclear
?s=laptop
?cat=e
?pmin=1000000
?sb=p&so=a
// 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
// ✓ 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"
}
Nên document rõ ràng:
/**
* 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
*/
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:
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...
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
}
});
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
*/
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
});
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
}
});
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;
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);
});