← Trở về trang chủ

Path Parameters

Định nghĩa

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ể.

Cú pháp

Pattern definition:

/users/{id}
/users/:id

Actual URL:

/users/123
/posts/456/comments/789

Phân tích:

  • {id} hoặc :id - Placeholder cho parameter
  • 123 - Giá trị thực tế của parameter
  • Parameter là phần của URL path, không phải query string

Mục đích

  • Xác định resource cụ thể: User #123, Post #456
  • Hierarchical structure: User's posts, Post's comments
  • RESTful routing: Theo chuẩn REST API
  • SEO-friendly URLs: Clean, readable URLs
  • Required parameters: Thường là bắt buộc

Ví dụ cơ bản

// 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

Playwright - Path Parameters

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);
});

Single Path Parameter

// Pattern
/users/{id}
/products/{sku}
/orders/{orderId}

// Actual URLs
/users/123
/products/ABC-123
/orders/ORD-2025-001

Playwright Examples

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);
  }
});

Multiple Path Parameters

// 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

Playwright - Multiple Parameters

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');
});

RESTful Naming Conventions

✓ Best Practices

// ✓ 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

✗ Bad Practices

// ✗ 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

Các định dạng Path Parameter phổ biến

1. Numeric ID

/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;
}

2. UUID

/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;
}

3. Slug (URL-friendly string)

/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;
}

4. Alphanumeric

/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');
}

Playwright - Testing Different Formats

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);
});

So sánh Path Parameters và Query Parameters

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

Khi dùng Path Parameters

  • ✓ Resource identification (required)
  • ✓ Hierarchical resources (user's posts)
  • ✓ Single resource operations (GET, PUT, DELETE specific item)
  • ✓ SEO-important URLs
  • ✓ Clean, readable URLs

Khi dùng Query Parameters

  • ✓ Optional filters
  • ✓ Sorting, pagination
  • ✓ Search queries
  • ✓ Multiple selection/filters
  • ✓ Configuration options

Ví dụ kết hợp Path và Query Parameters

// 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

Test với Playwright

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);
});

⚠️ Common Mistake: Mixing Concerns

// ✗ 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

CRUD với Path Parameters

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

Playwright - Complete CRUD

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);
  });
});

Hierarchical Resources

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 Nested Resources

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);
});
⚠️ Lưu ý về Nested Routes:
  • Không nên nest quá sâu (max 2-3 levels)
  • Verify parent resource exists
  • Check ownership/permissions

Resource Actions

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');
});

Key Takeaways

✓ Best Practices

  • Use path params for resource identification: /users/123
  • Use query params for filtering: /users?status=active
  • Keep URLs short and meaningful
  • Use nouns, not verbs: /users/123 not /getUser/123
  • Plural for collections: /users not /user
  • Max 3-4 levels deep: /users/{id}/posts/{postId}
  • Always validate parameter format
  • Return 404 if resource not found
  • Check authorization before accessing

Parameter Format Examples

  • Numeric: /users/123
  • UUID: /sessions/550e8400-e29b-41d4-a716-446655440000
  • Slug: /blog/my-first-post
  • Alphanumeric: /products/ABC-123

✗ Common Mistakes to Avoid

  • Using verbs in URLs: /getUser/123
  • Too deep nesting: /a/b/c/d/e/f
  • No validation: SQL injection risk
  • Mixing path and query params incorrectly
  • Using query params for required IDs
  • Inconsistent naming conventions

Quick Reference - Testing Pattern

import { 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);
});