25. Pattern: Filter Collection by Identifier

The Filter Collection by Identifier pattern allows clients to retrieve a specific batch of resources from a collection endpoint using a comma-separated list of IDs in a query parameter (e.g., GET /products?ids=1,4,6,7,8).

Instead of forcing clients to issue multiple individual HTTP requests (GET /products/1, GET /products/4, etc.) or abusing POST endpoints for read-only batch fetches, this pattern provides an efficient, idempotent, and cacheable mechanism for fetching targeted subsets of a resource collection in a single round-trip.

25.1. Overview

Frontend and mobile client applications frequently need to display arbitrary sets of resources that are already known by ID—such as items in a shopping cart, a user’s favorited products, pinned dashboard widgets, or recent notification records.

Without a batch filtering pattern, client applications fall into the N+1 HTTP Request Problem, opening dozens of concurrent TCP connections or firing sequential requests to populate a single UI view.

The Filter Collection by Identifier pattern solves this cleanly within standard REST semantics:

  • The resource collection endpoint (/products) accepts a query parameter (typically ids or id).
  • The parameter accepts a comma-separated array of values (ids=prd_101,prd_104,prd_108) or repeated query parameters (id=prd_101&id=prd_104).
  • The server performs a single batch query (e.g., WHERE id IN (...)) and returns an array containing the matching objects.

These operations:

  • Eliminate N+1 round-trip latency overhead over mobile and high-latency networks
  • Preserve standard HTTP caching semantics (GET requests remain safe and cacheable)
  • Keep API client integration code simple and maintainable

25.2. When to Use Filter Collection by Identifier

Use Filter Collection by Identifier when:

  • Clients need to fetch a known list of non-sequential resource IDs in batch.
    • e.g., Fetching product details for 10 items stored in a user’s browser-based shopping cart (GET /products?ids=p1,p4,p9).
  • Resolving the N+1 HTTP request problem.
    • Replacing a loop of individual GET /resources/{id} calls with a single GET /resources?ids=... request.
  • Fetching relational cross-references in bulk.
    • e.g., An order object contains an array of user IDs ([usr_12, usr_45]); the client fetches user profile details for all referenced actors in one call (GET /users?ids=usr_12,usr_45).
  • The batch size is small to moderate (typically under 50–100 IDs) and fits easily within standard HTTP URI character limits.

25.3. When NOT to Use Filter Collection by Identifier

Avoid Filter Collection by Identifier when:

  • The list of requested IDs is extremely large (e.g., hundreds or thousands of IDs).
    • Web servers, proxies, and CDNs typically enforce URI length limits (e.g., 2,048 to 8,192 bytes). Exceeding this limit results in 414 URI Too Long errors. For massive batch lookups, use a Search / Batch Query endpoint with a POST body instead.
  • Clients need to fetch a single resource by ID.
    • Fetching a single item should always use standard single-resource endpoints (GET /products/prd_101).
  • Filtering by dynamic domain attributes rather than explicit keys.
    • For filtering by property values (e.g., ?category=electronics&price_lt=100), use standard query filtering rather than explicit ID list filtering.

25.4. What the Pattern Looks Like

Below are HTTP request and response interaction flows demonstrating batch ID filtering, handling missing IDs, and handling URI length limits.

1. Standard Batch Request

The client requests three specific product records in a single call.

Request

GET /products?ids=prd_101,prd_104,prd_108 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...

Response

The server returns a JSON array containing only the requested items.

HTTP/1.1 200 OK
Cache-Control: private, max-age=300
Content-Type: application/json

{
  "items": [
    {
      "id": "prd_101",
      "name": "Wireless Ergonomic Keyboard",
      "price": 89.99,
      "inStock": true
    },
    {
      "id": "prd_104",
      "name": "Precision Optical Mouse",
      "price": 34.50,
      "inStock": true
    },
    {
      "id": "prd_108",
      "name": "USB-C Multi-Port Hub",
      "price": 45.00,
      "inStock": false
    }
  ],
  "total": 3
}

2. Partial Match Handling (Non-existent IDs)

If a client requests three IDs (prd_101,prd_999,prd_108) and prd_999 has been deleted or does not exist, the API returns 200 OK with the matching records.

Request

GET /products?ids=prd_101,prd_999,prd_108 HTTP/1.1
Host: api.example.com

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [
    {
      "id": "prd_101",
      "name": "Wireless Ergonomic Keyboard",
      "price": 89.99
    },
    {
      "id": "prd_108",
      "name": "USB-C Multi-Port Hub",
      "price": 45.00
    }
  ],
  "total": 2
}

(Note: Returning matching items with 200 OK is standard REST behavior for collection filtering. Do not fail the entire batch with 404 Not Found if at least one item is missing).

3. Exceeding URL Character Limits (414 URI Too Long)

If a client sends an excessively long query string exceeding server limits.

Response

HTTP/1.1 414 URI Too Long
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/uri-too-long",
  "title": "URI Too Long",
  "status": 414,
  "detail": "The request URI exceeds the maximum limit of 2048 characters. Reduce the number of requested IDs or use batch query."
}

25.5. Anti-Patterns to Avoid

1. The N+1 HTTP fetch loop

// Anti-Pattern: Making individual network calls in a loop
const cartItems = ['prd_101', 'prd_104', 'prd_108'];
const products = await Promise.all(cartItems.map(id => fetch(`/products/${id}`)));
  • Why it’s bad: Spawns multiple TCP/TLS connections, increases server connection overhead, exhausts browser socket pools, and severely degrades performance on mobile devices. Use GET /products?ids=prd_101,prd_104,prd_108.

2. Using POST for simple, read-only batch queries

/* Anti-Pattern: Using POST for standard GET filtering */
POST /products/batch-get HTTP/1.1
Content-Type: application/json

{
  "ids": ["prd_101", "prd_104", "prd_108"]
}
  • Why it’s bad: POST is non-idempotent and non-cacheable by default in browsers, CDNs, and HTTP proxies. Use POST only when payload size genuinely exceeds HTTP URI length thresholds (~2KB).

3. Returning 404 Not Found when a single batch item is missing

  • If a client requests 10 IDs and 1 item no longer exists, returning 404 Not Found destroys the utility of the entire request. Return 200 OK with the 9 matching records.

4. Failing to set max array length limits in OpenAPI

  • Allowing an unbounded ids parameter without defining maxItems in your OpenAPI spec allows clients to pass thousands of keys, leading to unoptimized database IN (...) queries and memory exhaustion.

25.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating how to document a collection endpoint supporting batch filtering by comma-separated IDs using style: form and explode: false.

openapi: 3.0.3
info:
  title: Product Catalog API - Filter Collection by Identifier Example
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /products:
    get:
      summary: List products with optional batch ID filtering
      description: Returns a collection of products. Supports filtering by a comma-separated list of specific product IDs.
      tags: [Products]
      parameters:
        - in: query
          name: ids
          required: false
          style: form
          explode: false
          schema:
            type: array
            maxItems: 50
            items:
              type: string
            example: ["prd_101", "prd_104", "prd_108"]
          description: Comma-separated list of product IDs to retrieve in batch (e.g. ?ids=prd_101,prd_104,prd_108). Maximum 50 IDs.
      responses:
        '200':
          description: Collection of products matching the criteria
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductCollectionResponse'
        '414':
          description: URI Too Long - Requested query string exceeded server length limits
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

components:
  schemas:
    ProductCollectionResponse:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Product'
        total:
          type: integer
          example: 3

    Product:
      type: object
      required:
        - id
        - name
        - price
      properties:
        id:
          type: string
          example: prd_101
        name:
          type: string
          example: Wireless Ergonomic Keyboard
        price:
          type: number
          format: float
          example: 89.99
        inStock:
          type: boolean
          example: true

    ProblemDetails:
      type: object
      properties:
        type:
          type: string
          example: "https://api.example.com/errors/uri-too-long"
        title:
          type: string
          example: "URI Too Long"
        status:
          type: integer
          example: 414
        detail:
          type: string
          example: "The request URI exceeds the maximum limit."

25.7. Visualizing Filter Collection by Identifier (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor Client as Client App (Cart View)
    participant API as API Server / Gateway
    participant DB as Database

    Note over Client,DB: Problem: N+1 HTTP Requests (Avoided)
    Note over Client: Client holds cart keys: ["prd_101", "prd_104", "prd_108"]

    Note over Client,DB: Solution: Single Batch Collection Filter
    Client->>API: GET /products?ids=prd_101,prd_104,prd_108
    API->>API: Parse query param 'ids' -> Array["prd_101", "prd_104", "prd_108"]
    API->>DB: SELECT * FROM products WHERE id IN ('prd_101', 'prd_104', 'prd_108')
    DB-->>API: Return matching rows (3 records)
    
    API-->>Client: 200 OK { items: [...3 products], total: 3 }
    Note over Client: Client renders full cart view in 1 network round-trip!