Query Parameters

Key-value pairs appended to URLs for filtering, sorting, and customizing API requests.

Query parameters are key-value pairs appended to a URL after the ? character, used to filter, sort, paginate, or modify API responses. They provide a flexible way to customize requests without changing the endpoint structure.

Syntax and Structure

Basic format: /users?status=active&limit=10

  • ? starts the query string
  • key=value pairs define parameters
  • & separates multiple parameters
  • Values must be URL-encoded (spaces become %20 or +)

Multiple values: /products?category=electronics&category=books or /products?category=electronics,books

Common Use Cases

PurposeExampleDescription
Filtering?status=activeReturn only matching records
Sorting?sort=created_at&order=descOrder results
Pagination?page=2&limit=20Navigate through results
Search?q=search+termFull-text search
Field selection?fields=id,name,emailReturn only specific fields
Include relations?include=posts,commentsExpand related data

Query Params vs Path Params

Path parameters identify specific resources: GET /users/123 - Get user with ID 123

Query parameters modify the response: GET /users?status=active - Get all active users

Rule of thumb:

  • Path params for required identifiers
  • Query params for optional filters/modifiers

Designing Query Parameters

Be consistent: Pick a convention and stick to it. sort_by or sortBy, not both.

Use intuitive names: ?limit=10 not ?l=10. Readability over brevity.

Document defaults: What happens when parameter is omitted? Document it.

Validate inputs: Reject invalid values with clear error messages.

Set sensible limits: ?limit=10000 could crash your server. Cap it.

Pagination Patterns

Offset-based: ?page=2&limit=20 or ?offset=20&limit=20 Simple but slow for large offsets (database must skip rows).

Cursor-based: ?cursor=abc123&limit=20 Fast for large datasets. Cursor is usually encoded ID or timestamp.

Choose cursor when:

  • Large datasets (100k+ records)
  • Real-time data (items added frequently)
  • Infinite scroll UI

Filtering Best Practices

Range filters: ?price_min=10&price_max=100 or ?price[gte]=10&price[lte]=100

Multiple values: ?status=active,pending (comma-separated) or ?status[]=active&status[]=pending (array syntax)

Null checks: ?deleted_at=null or ?deleted_at[is]=null

Nested filtering: ?user.role=admin or ?filter[user][role]=admin

Security Considerations

SQL Injection: Never concatenate query params directly into SQL. Use parameterized queries.

Sensitive data: Query params appear in logs, browser history, referrer headers. Don't pass passwords or tokens.

Mass assignment: Don't blindly pass all query params to database queries. Whitelist allowed fields.

DoS prevention: Limit maximum page size, search complexity, and number of filters.

Common Mistakes

1. Inconsistent naming: sortBy on one endpoint, sort_by on another. Pick one convention.

2. No validation: ?limit=abc should return 400, not crash.

3. Missing defaults: What's the default sort order? Document and handle it.

4. Case sensitivity: ?Status=Active vs ?status=active. Be explicit about case handling.

5. Ignoring URL length limits: URLs over 2048 chars may fail. Use POST with body for complex queries.

Code Examples

Parsing and Validating Query Parameters

// Express route with query parameter handling
app.get('/products', (req, res) => {
  // Parse with defaults
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, parseInt(req.query.limit) || 20);
  const sortBy = ['price', 'name', 'created_at'].includes(req.query.sort)
    ? req.query.sort
    : 'created_at';
  const order = req.query.order === 'asc' ? 'asc' : 'desc';

  // Parse array filters
  const categories = req.query.category
    ? [].concat(req.query.category)
    : [];

  // Build query
  const offset = (page - 1) * limit;
  const products = await db.products.findMany({
    where: categories.length
      ? { category: { in: categories } }
      : undefined,
    orderBy: { [sortBy]: order },
    skip: offset,
    take: limit
  });

  res.json({
    data: products,
    pagination: { page, limit, total: await db.products.count() }
  });
});