API Design

API Filtering and Sorting

Conventions for filtering collections and sorting results in REST APIs — LHS bracket notation, RHS colon notation, multi-field sort syntax, sparse fieldsets, partial responses, comparison operators, GraphQL field selection vs REST partial responses, and documenting query parameters in OpenAPI.

⏱ 11 min read

What it is

Filtering and sorting are query capabilities on collection endpoints that let clients retrieve the subset of data they need without receiving the entire collection. They work alongside pagination: filtering reduces the total result set; sorting determines the order; pagination retrieves it in pages.

Unlike pagination, which has established specifications (Relay, RFC 5988), filtering and sorting conventions are largely community-driven, with several competing styles. The goal is to choose a consistent, secure, and indexable approach for your API and document it thoroughly in OpenAPI.

Filtering approaches

Simple equality filters

GET /orders?status=confirmed
GET /orders?customerId=42&status=pending

The simplest approach: map query parameters directly to field values. Sufficient for equality filters on a known set of filterable fields. Explicitly allowlist filterable fields — never pass arbitrary query parameters directly to a database query.

LHS bracket notation

GET /orders?total[gte]=100&total[lte]=500
GET /orders?createdAt[gte]=2024-01-01&status[in][]=pending&status[in][]=confirmed

The Left-Hand Side bracket notation encodes the operator as a bracket suffix on the field name. Used by Loopback, JSON:API filter implementations. Operators: [eq], [ne], [gt], [gte], [lt], [lte], [in], [nin], [like], [ilike].

RHS colon notation

GET /orders?filter=total:gte:100,total:lte:500
GET /orders?filter=status:in:pending|confirmed
GET /orders?total=gte:100&createdAt=gte:2024-01-01

The Right-Hand Side colon notation places the operator in the value: field=operator:value. Popularised by some API gateway implementations and custom DSLs. Less portable but compact for complex filters.

FIQL (Feed Item Query Language)

GET /orders?q=status==confirmed;total=gt=100
GET /orders?q=status==pending,status==confirmed  (OR)

FIQL (RFC draft) uses ==, !=, =gt=, =lt=, =ge=, =le= operators with ; for AND and , for OR. Used by Apache CXF and some feed/event APIs. Expressive but requires a dedicated parser.

Filter as JSON body

GET /orders
Content-Type: application/json
{
  "filter": {
    "status": { "$in": ["pending", "confirmed"] },
    "total": { "$gte": 100, "$lte": 500 },
    "createdAt": { "$gte": "2024-01-01" }
  }
}

A JSON body filter (MongoDB-style syntax) is expressive and supports arbitrary nesting. However, GET requests with bodies violate HTTP semantics and break caching. Use POST to a search-specific endpoint (POST /orders/search) when complex filter expressions are required. See Elasticsearch's Query DSL as a reference implementation.

Sorting conventions

Single field ascending:
  GET /orders?sort=createdAt

Single field descending (prefix with minus):
  GET /orders?sort=-createdAt

Multiple fields (comma-separated, precedence left to right):
  GET /orders?sort=-createdAt,id

Alternative (sort and order as separate params):
  GET /orders?sortBy=createdAt&order=desc

Alternative (repeated sort param):
  GET /orders?sort[]=createdAt:desc&sort[]=id:asc

The -field prefix for descending is the most concise and widely adopted convention (used by JSON:API, Stripe, GitHub). Ensure all sortable fields have database indexes. For pagination stability, always include a tiebreaker (e.g., unique id) as the final sort key.

Allowlist sortable fields — accepting arbitrary sort values and passing them to ORDER BY is a SQL injection risk. Define explicitly which fields are sortable.

Sparse fieldsets and partial responses

REST partial response (Google API style):
  GET /orders/42?fields=id,status,total,customer.name
  → { "id": "42", "status": "confirmed", "total": 149.99,
      "customer": { "name": "Alice" } }

JSON:API sparse fieldsets:
  GET /articles?fields[articles]=title,body&fields[people]=name

GraphQL field selection (always explicit):
  query {
    order(id: "42") {
      id
      status
      total
      customer { name }
    }
  }

Sparse fieldsets allow clients to request only the fields they need, reducing payload size and improving performance for bandwidth-constrained clients. GraphQL makes field selection mandatory and structural. In REST, partial responses are opt-in via a fields query parameter. Implement with a projection list — never dynamically construct SELECT clauses from unvalidated input.

Security considerations

  • Allowlist filterable and sortable fields — reject unrecognised filter parameters with 400, not silently ignore them. Never pass unvalidated input to a SQL WHERE or ORDER BY clause.
  • Validate filter operators — only allow operators explicitly supported for each field type (e.g., like on an indexed text field, not on a UUID).
  • Authorisation-aware filtering — a customerId filter must be enforced server-side regardless of what value the caller provides — never trust the caller to self-scope their query.
  • Limit filter complexity — unbounded deeply nested filter expressions can cause expensive query plans; cap nesting depth and number of conditions.

OpenAPI documentation

paths:
  /orders:
    get:
      parameters:
        - name: status
          in: query
          description: Filter by order status. Multiple values are OR'd.
          schema:
            type: array
            items:
              type: string
              enum: [pending, confirmed, shipped, delivered, cancelled]
          style: form
          explode: true
        - name: "total[gte]"
          in: query
          description: Filter orders with total greater than or equal to value.
          schema:
            type: number
            minimum: 0
        - name: sort
          in: query
          description: |
            Sort field. Prefix with - for descending.
            Examples: sort=createdAt (ascending), sort=-createdAt (descending).
            Comma-separated for multi-field: sort=-createdAt,id
          schema:
            type: string
            pattern: "^-?[a-zA-Z]+(,-?[a-zA-Z]+)*$"
          examples:
            newest_first:
              value: "-createdAt"
            oldest_first:
              value: "createdAt"

Pros and cons

Pros of structured filter params

  • Declarative — clients express intent without knowing the backend query language.
  • Cacheable — GET requests with query params are cached by HTTP caches and CDNs.
  • Easy to document in OpenAPI with specific operators per field.
  • Stateless — filter state is entirely in the URL; URLs are shareable and bookmarkable.

Cons

  • No single standard — LHS brackets, RHS colons, FIQL all exist; pick one and document it.
  • Complex boolean logic (AND/OR/NOT combinations) is awkward in query params; use POST /search.
  • URL length limits constrain complex filter expressions — browsers cap URLs at ~2,000 chars.
  • Maintaining index coverage for every filter combination is an ongoing DBA challenge.

Decision checklist

  • Are filterable and sortable fields explicitly allowlisted (not dynamic)?
  • Are database indexes in place for the most common filter/sort combinations?
  • Is the filter convention (LHS bracket, RHS colon, simple equality) consistently applied across all endpoints?
  • Are all filter parameters documented in OpenAPI with examples?
  • Are server-side authorisation constraints applied regardless of filter values (e.g., customer scoping)?
  • For complex search requirements, is POST /resource/search used instead of an unwieldy GET?
  • API Pagination — filtering determines the total result set; pagination retrieves it in pages.
  • REST API Design — filtering is a core collection query capability.
  • OpenAPI Specification — document all filter parameters and their operators.
  • GraphQL — field selection and filtering are first-class in GraphQL query language.

Further reading

  • JSON:API Filtering — jsonapi.org/recommendations/#filtering
  • Google API Design Guide — filtering and sorting — cloud.google.com/apis/design/standard_fields
  • Microsoft REST API Guidelines — filtering — github.com/microsoft/api-guidelines
  • Elasticsearch Query DSL — reference for POST-based search endpoints.