Path Parameters là các biến được nhúng trực tiếp trong URL path, dùng để xác định resource cụ thể.
Pattern definition:
Actual URL:
Phân tích:
{id} hoặc :id - Placeholder cho parameter123 - Giá trị thực tế của parameter// Get user by ID
GET /users/123
// Get specific post
GET /posts/456
// Get comment on a post
GET /posts/456/comments/789
// Update user
PUT /users/123
// Delete product
DELETE /products/ABC-123
import { test, expect } from '@playwright/test';
test('GET user by ID', async ({ request }) => {
const userId = 123;
const response = await request.get(`https://api.example.com/users/${userId}`);
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.id).toBe(userId);
});
test('GET nested resource', async ({ request }) => {
const userId = 123;
const postId = 456;
const response = await request.get(
`https://api.example.com/users/${userId}/posts/${postId}`
);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.id).toBe(postId);
expect(post.userId).toBe(userId);
});
test('DELETE resource', async ({ request }) => {
const productId = 'ABC-123';
const response = await request.delete(
`https://api.example.com/products/${productId}`
);
expect(response.status()).toBe(204);
});
// Pattern
/users/{id}
/products/{sku}
/orders/{orderId}
// Actual URLs
/users/123
/products/ABC-123
/orders/ORD-2025-001
import { test, expect } from '@playwright/test';
test('Single numeric ID', async ({ request }) => {
const response = await request.get('https://api.example.com/users/123');
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.id).toBe(123);
});
test('Single string SKU', async ({ request }) => {
const sku = 'ABC-123';
const response = await request.get(`https://api.example.com/products/${sku}`);
expect(response.status()).toBe(200);
const product = await response.json();
expect(product.sku).toBe(sku);
});
test('Dynamic parameter value', async ({ request }) => {
const orderIds = ['ORD-001', 'ORD-002', 'ORD-003'];
for (const orderId of orderIds) {
const response = await request.get(
`https://api.example.com/orders/${orderId}`
);
expect(response.status()).toBe(200);
const order = await response.json();
expect(order.id).toBe(orderId);
}
});
// Pattern
/users/{userId}/posts/{postId}
/countries/{country}/cities/{city}
/organizations/{orgId}/teams/{teamId}
// Actual URLs
/users/123/posts/456
/countries/vietnam/cities/hanoi
/organizations/org-1/teams/team-5
test('Multiple path parameters', async ({ request }) => {
const userId = 123;
const postId = 456;
const response = await request.get(
`https://api.example.com/users/${userId}/posts/${postId}`
);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.id).toBe(postId);
expect(post.userId).toBe(userId);
});
test('3-level hierarchy', async ({ request }) => {
const orgId = 'org-1';
const teamId = 'team-5';
const memberId = 'member-10';
const response = await request.get(
`https://api.example.com/organizations/${orgId}/teams/${teamId}/members/${memberId}`
);
expect(response.status()).toBe(200);
const member = await response.json();
expect(member.id).toBe(memberId);
expect(member.teamId).toBe(teamId);
expect(member.organizationId).toBe(orgId);
});
test('String path parameters', async ({ request }) => {
const country = 'vietnam';
const city = 'hanoi';
const response = await request.get(
`https://api.example.com/countries/${country}/cities/${city}`
);
expect(response.status()).toBe(200);
const cityData = await response.json();
expect(cityData.name).toBe('Hanoi');
expect(cityData.country).toBe('vietnam');
});
// ✓ Use plural for collections
/users/{id} // NOT /user/{id}
/products/{sku} // NOT /product/{sku}
/posts/{postId} // NOT /post/{postId}
// ✓ Descriptive parameter names
/users/{userId} // Clear
/posts/{postId} // Clear
/orders/{orderId} // Clear
// ✓ kebab-case for multi-word paths
/order-items/{id}
/user-preferences/{id}
/payment-methods/{id}
// ✓ Lowercase URLs
/users/123 // NOT /Users/123
/products/abc // NOT /Products/ABC
// ✗ Using verbs
/getUser/123 // Use GET /users/123
/createPost // Use POST /posts
/updateProduct/456 // Use PUT /products/456
// ✗ Unclear parameter names
/users/{x} // What is x?
/posts/{p} // What is p?
// ✗ Inconsistent naming
/users/123
/product/456 // Should be /products/456
/Order/789 // Should be /orders/789
// ✗ Mixed case
/Users/123
/PRODUCTS/abc
| Convention | Example | Notes |
|---|---|---|
| Plural resources | /users/{id} | Phổ biến nhất |
| Descriptive params | /users/{userId} | Self-documenting |
| kebab-case | /order-items/{id} | For multi-word |
| Lowercase | /products/abc | URL consistency |
| Nouns not verbs | /users (not /getUsers) | RESTful principle |
/users/123
/posts/456789
/orders/1
Validation:
// Playwright
const userId = 123;
if (!Number.isInteger(userId) || userId <= 0) {
throw new Error('Invalid user ID');
}
// PHP
if (!is_numeric($id) || $id <= 0) {
http_response_code(400);
exit;
}
/users/550e8400-e29b-41d4-a716-446655440000
/sessions/123e4567-e89b-12d3-a456-426614174000
Validation:
// Regex pattern for UUID v4
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(uuid)) {
throw new Error('Invalid UUID');
}
// PHP
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuid)) {
http_response_code(400);
exit;
}
/blog/my-first-blog-post
/blog/introduction-to-api-testing
/products/gaming-laptop-dell-g15
Validation:
// Slug pattern: lowercase letters, numbers, hyphens
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (!slugRegex.test(slug)) {
throw new Error('Invalid slug');
}
// PHP
if (!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug)) {
http_response_code(400);
exit;
}
/products/ABC123
/orders/ORD-2025-001
/invoices/INV-20250115-001
Validation:
// Allow letters, numbers, hyphens
const alphanumericRegex = /^[A-Z0-9-]+$/i;
if (!alphanumericRegex.test(code)) {
throw new Error('Invalid code');
}
test('Numeric ID format', async ({ request }) => {
const response = await request.get('https://api.example.com/users/123');
expect(response.status()).toBe(200);
});
test('UUID format', async ({ request }) => {
const uuid = '550e8400-e29b-41d4-a716-446655440000';
const response = await request.get(`https://api.example.com/sessions/${uuid}`);
expect(response.status()).toBe(200);
});
test('Slug format', async ({ request }) => {
const slug = 'my-first-blog-post';
const response = await request.get(`https://api.example.com/blog/${slug}`);
expect(response.status()).toBe(200);
});
test('Invalid format returns 400', async ({ request }) => {
// Invalid numeric ID
const response1 = await request.get('https://api.example.com/users/abc');
expect(response1.status()).toBe(400);
// Invalid UUID
const response2 = await request.get('https://api.example.com/sessions/not-a-uuid');
expect(response2.status()).toBe(400);
// Invalid slug (has uppercase)
const response3 = await request.get('https://api.example.com/blog/My-Post');
expect(response3.status()).toBe(400);
});
| Feature | Path Parameters | Query Parameters |
|---|---|---|
| Purpose | Identify resource | Filter/modify request |
| Required | Usually YES | Usually NO |
| Position | In URL path | After ? |
| Example | /users/123 | /users?status=active |
| SEO-friendly | Yes | Less |
| Cacheable | Yes | Yes |
| Multiple values | Nested paths | Easy with & |
| Hierarchy | Natural | Flat |
// Path param: User ID (required, identifies resource)
// Query params: Status filter and pagination (optional)
GET /users/123/posts?status=published&limit=10&page=2
↑ ↑
Path param Query params
// Path params: Organization and Team (hierarchy)
// Query param: Role filter (optional)
GET /organizations/org-1/teams/team-5/members?role=admin
↑ ↑ ↑
Path params Query param
import { test, expect } from '@playwright/test';
test('Path param + Query params', async ({ request }) => {
const userId = 123;
const response = await request.get(
`https://api.example.com/users/${userId}/posts`,
{
params: {
status: 'published',
limit: 10,
page: 2,
sort: 'created_at',
order: 'desc'
}
}
);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.userId).toBe(userId);
expect(data.posts).toBeInstanceOf(Array);
});
// ✗ BAD: Required ID in query param
GET /users?id=123
// ✓ GOOD: ID in path param
GET /users/123
// ✗ BAD: Optional filter in path
GET /users/filter/active
// ✓ GOOD: Filter in query param
GET /users?status=active
// ✓ BEST: Combine both properly
GET /users/123?include=posts,comments
GET /users/{id} # Read - Get user by ID
POST /users # Create - No path param (ID generated by server)
PUT /users/{id} # Update - Full update
PATCH /users/{id} # Update - Partial update
DELETE /users/{id} # Delete - Remove user
import { test, expect } from '@playwright/test';
test.describe('CRUD Operations', () => {
let createdUserId;
test('CREATE - POST /users', async ({ request }) => {
const response = await request.post('https://api.example.com/users', {
data: {
name: 'Nguyễn Văn A',
email: 'vana@example.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user).toHaveProperty('id');
createdUserId = user.id;
});
test('READ - GET /users/{id}', async ({ request }) => {
const response = await request.get(
`https://api.example.com/users/${createdUserId}`
);
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.id).toBe(createdUserId);
expect(user.name).toBe('Nguyễn Văn A');
});
test('UPDATE - PUT /users/{id}', async ({ request }) => {
const response = await request.put(
`https://api.example.com/users/${createdUserId}`,
{
data: {
name: 'Nguyễn Văn A Updated',
email: 'vana_new@example.com'
}
}
);
expect(response.status()).toBe(200);
});
test('PARTIAL UPDATE - PATCH /users/{id}', async ({ request }) => {
const response = await request.patch(
`https://api.example.com/users/${createdUserId}`,
{
data: {
name: 'Nguyễn Văn A Final'
}
}
);
expect(response.status()).toBe(200);
});
test('DELETE - DELETE /users/{id}', async ({ request }) => {
const response = await request.delete(
`https://api.example.com/users/${createdUserId}`
);
expect(response.status()).toBe(204);
// Verify deleted
const getResponse = await request.get(
`https://api.example.com/users/${createdUserId}`
);
expect(getResponse.status()).toBe(404);
});
});
GET /users/{userId}/posts # Get all posts of user
GET /users/{userId}/posts/{postId} # Get specific post
POST /users/{userId}/posts # Create post for user
PUT /users/{userId}/posts/{postId} # Update post
DELETE /users/{userId}/posts/{postId} # Delete post
GET /posts/{postId}/comments # Get all comments on post
GET /posts/{postId}/comments/{commentId} # Get specific comment
POST /posts/{postId}/comments # Add comment to post
GET /orders/{orderId}/items # Get order items
GET /orders/{orderId}/items/{itemId} # Get specific item
test('Get user posts', async ({ request }) => {
const userId = 123;
const response = await request.get(
`https://api.example.com/users/${userId}/posts`
);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.posts).toBeInstanceOf(Array);
// Verify all posts belong to this user
data.posts.forEach(post => {
expect(post.userId).toBe(userId);
});
});
test('Get specific user post', async ({ request }) => {
const userId = 123;
const postId = 456;
const response = await request.get(
`https://api.example.com/users/${userId}/posts/${postId}`
);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.id).toBe(postId);
expect(post.userId).toBe(userId);
});
test('Create post for user', async ({ request }) => {
const userId = 123;
const response = await request.post(
`https://api.example.com/users/${userId}/posts`,
{
data: {
title: 'New Post',
content: 'Post content here'
}
}
);
expect(response.status()).toBe(201);
const post = await response.json();
expect(post.userId).toBe(userId);
});
POST /users/{userId}/activate # Activate user
POST /users/{userId}/deactivate # Deactivate user
POST /users/{userId}/reset-password # Reset password
POST /orders/{orderId}/cancel # Cancel order
POST /orders/{orderId}/ship # Ship order
PUT /posts/{postId}/publish # Publish post
POST /invoices/{invoiceId}/send # Send invoice
test('Activate user', async ({ request }) => {
const userId = 123;
const response = await request.post(
`https://api.example.com/users/${userId}/activate`
);
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.status).toBe('active');
});
test('Cancel order', async ({ request }) => {
const orderId = 'ORD-001';
const response = await request.post(
`https://api.example.com/orders/${orderId}/cancel`,
{
data: {
reason: 'Customer request'
}
}
);
expect(response.status()).toBe(200);
const order = await response.json();
expect(order.status).toBe('cancelled');
});
/getUser/123/a/b/c/d/e/fimport { test, expect } from '@playwright/test';
// Basic pattern
test('Path parameter test', async ({ request }) => {
const resourceId = 123;
const response = await request.get(
`https://api.example.com/resource/${resourceId}`
);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.id).toBe(resourceId);
});
// Nested pattern
test('Nested resource test', async ({ request }) => {
const userId = 123;
const postId = 456;
const response = await request.get(
`https://api.example.com/users/${userId}/posts/${postId}`
);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.id).toBe(postId);
expect(post.userId).toBe(userId);
});
// With query params
test('Path + query params', async ({ request }) => {
const userId = 123;
const response = await request.get(
`https://api.example.com/users/${userId}/posts`,
{
params: {
status: 'published',
limit: 10
}
}
);
expect(response.status()).toBe(200);
});
// Error handling
test('404 for non-existent resource', async ({ request }) => {
const response = await request.get(
'https://api.example.com/users/99999'
);
expect(response.status()).toBe(404);
});
test('400 for invalid format', async ({ request }) => {
const response = await request.get(
'https://api.example.com/users/invalid-id'
);
expect(response.status()).toBe(400);
});