The Query / Search Model pattern is used when consumers need flexible, read-only access to data via search, filtering, sorting, and projections — often beyond what a simple CRUD GET /resource can support.

7.1. Overview

Instead of trying to bolt complex queries onto CRUD endpoints, you expose dedicated search/read endpoints such as:

  • POST /search-orders
  • POST /search-customers

These are read-only but query-rich, returning projections optimized for clients and UX.

Query / Search Model endpoints:

  • Provide search & filtering capabilities tailored to how consumers need to explore data.
  • Return read-optimized projections (summaries, denormalized views) rather than full aggregates.
  • Are read-only — they never modify server state.
  • May use POST with a body (POST /search-orders) to support:

    • Complex filters (nested conditions, ranges, arrays)
    • Faceted search (filters + aggregations)
    • Sensitive search criteria (PII/NPI) that we don’t want in URLs.

They frequently coexist with:

  • CRUD reads (e.g., GET /orders/{orderId}) for single-entity detail
  • Extended CRUD / Functional Resources for workflows and commands

7.2. Why POST /search-orders (Not GET /orders with Query Params)?

You can do simple queries with GET /orders?status=..., but POST /search-orders is preferred when:

  • Search criteria are complex

    • Multiple nested filters, arrays, ranges, or expressions become unwieldy in query strings.
  • You need faceted search

    • e.g., filter + return counts by status, region, etc.
  • You are dealing with PII/NPI or sensitive data

    • Names, emails, account numbers, partial SSNs, etc. are safer in the request body than in URLs because:

      • URLs are often logged by:

        • Reverse proxies, load balancers, API gateways
        • Web servers and HTTP middleware
        • Browser history and analytics tools
      • Request bodies can be:

        • More selectively logged or redacted
        • Better controlled under data protection policies

Rule of thumb: Use GET with query params for simple, non-sensitive filters. Use POST with a body (e.g., POST /search-orders) for complex, faceted, or sensitive searches.

7.3. When to Use the Query / Search Model Pattern

Use this pattern when:

  • You are designing search, filter, and list experiences (e.g., admin grids, dashboards).
  • Consumers need to retrieve many entities at once based on flexible criteria.
  • You want to return summaries / projections instead of full detail.
  • Query performance and UX are important (e.g., read-optimized index, denormalized views).
  • You want a clean separation between:

    • “How we store data” (internal models)
    • “How we present data to consumers” (read models)

Example:

  • POST /search-orders → takes a SearchOrdersRequest with filters and pagination, returns OrderSearchResult projections.

7.4. When NOT to Use the Query / Search Model Pattern

Avoid this pattern when:

  • You only need simple “list all” for a small dataset and basic CRUD GET /resource is sufficient.
  • The endpoint is trying to perform business actions (that belongs in Extended CRUD or Functional Resources).
  • You’re implementing heavy analytics or BI; that might justify a separate reporting/analytics pattern or system.

Also avoid:

  • Overusing query endpoints as a catch-all for “I don’t want to design proper resources.”
  • Returning full aggregates with every search result when summaries would do.

7.5. What the Pattern Looks Like

Canonical shape:

  • POST /search-orders

    • Request body: filters, sort options, pagination
    • Response: list of OrderSearchResult projections + nextCursor

And, alongside it:

  • GET /orders/{orderId}

    • Returns full OrderDetail for a specific order

The contrast:

  • POST /search-orderssearch/read model (many, summary, rich filters)
  • GET /orders/{orderId}CRUD read (one, detailed)

7.6. OpenAPI Example

openapi: 3.0.3
info:
  title: Orders Search API - Query / Search Model Pattern Example
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /search-orders:
    post:
      summary: Search orders using flexible criteria
      tags: [Orders]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchOrdersRequest'
            examples:
              default:
                value:
                  status: ["SHIPPED", "PAID"]
                  customerId: "cus_12345"
                  dateRange:
                    fromDate: "2024-01-01"
                    toDate: "2024-12-31"
                  minTotalAmount: 1000
                  maxTotalAmount: 10000
                  sort:
                    field: "createdAt"
                    direction: "DESC"
                  limit: 20
                  cursor: "eyJpZCI6Im9yXzEyMzQ1In0="
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchOrdersResponse'
              examples:
                default:
                  value:
                    items:
                      - id: "or_12345"
                        customerId: "cus_12345"
                        createdAt: "2024-01-10T12:00:00Z"
                        currency: "USD"
                        totalAmount: 5500
                        status: "SHIPPED"
                      - id: "or_67890"
                        customerId: "cus_12345"
                        createdAt: "2024-01-12T09:30:00Z"
                        currency: "USD"
                        totalAmount: 3200
                        status: "PAID"
                    nextCursor: "eyJpZCI6Im9yXzY3ODkwIn0="

  /orders/{orderId}:
    get:
      summary: Get order details by ID
      tags: [Orders]
      parameters:
        - in: path
          name: orderId
          required: true
          schema:
            type: string
            example: "or_12345"
      responses:
        '200':
          description: Order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDetail'
              examples:
                default:
                  value:
                    id: "or_12345"
                    customerId: "cus_12345"
                    createdAt: "2024-01-10T12:00:00Z"
                    currency: "USD"
                    totalAmount: 5500
                    status: "SHIPPED"
                    items:
                      - sku: "BOOK-DDD-001"
                        quantity: 1
                        unitPrice: 5500
                    shippingAddress:
                      line1: "123 Market St"
                      city: "San Francisco"
                      region: "CA"
                      postalCode: "94103"
                      country: "US"
        '404':
          description: Not Found

components:
  schemas:
    SearchOrdersRequest:
      type: object
      properties:
        status:
          type: array
          items:
            type: string
            enum: [PENDING, PAID, SHIPPED, CANCELLED]
          example: ["SHIPPED", "PAID"]
        customerId:
          type: string
          example: "cus_12345"
        dateRange:
          type: object
          properties:
            fromDate:
              type: string
              format: date
              example: "2024-01-01"
            toDate:
              type: string
              format: date
              example: "2024-12-31"
        minTotalAmount:
          type: integer
          example: 1000
        maxTotalAmount:
          type: integer
          example: 10000
        sort:
          type: object
          properties:
            field:
              type: string
              example: "createdAt"
            direction:
              type: string
              enum: [ASC, DESC]
              example: "DESC"
        limit:
          type: integer
          minimum: 1
          maximum: 100
          example: 20
        cursor:
          type: string
          example: "eyJpZCI6Im9yXzEyMzQ1In0="
      required: []

    SearchOrdersResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderSearchResult'
        nextCursor:
          type: string
          nullable: true
          example: "eyJpZCI6Im9yXzY3ODkwIn0="
      required:
        - items

    OrderSearchResult:
      type: object
      properties:
        id:
          type: string
          example: "or_12345"
        customerId:
          type: string
          example: "cus_12345"
        createdAt:
          type: string
          format: date-time
          example: "2024-01-10T12:00:00Z"
        currency:
          type: string
          example: "USD"
        totalAmount:
          type: integer
          example: 5500
        status:
          type: string
          enum: [PENDING, PAID, SHIPPED, CANCELLED]
          example: "SHIPPED"
      required:
        - id
        - customerId
        - createdAt
        - currency
        - totalAmount
        - status

    OrderItem:
      type: object
      properties:
        sku:
          type: string
          example: "BOOK-DDD-001"
        quantity:
          type: integer
          example: 1
        unitPrice:
          type: integer
          example: 5500
      required:
        - sku
        - quantity
        - unitPrice

    Address:
      type: object
      properties:
        line1:
          type: string
          example: "123 Market St"
        city:
          type: string
          example: "San Francisco"
        region:
          type: string
          example: "CA"
        postalCode:
          type: string
          example: "94103"
        country:
          type: string
          example: "US"
      required:
        - line1
        - city
        - region
        - postalCode
        - country

    OrderDetail:
      type: object
      properties:
        id:
          type: string
          example: "or_12345"
        customerId:
          type: string
          example: "cus_12345"
        createdAt:
          type: string
          format: date-time
          example: "2024-01-10T12:00:00Z"
        currency:
          type: string
          example: "USD"
        totalAmount:
          type: integer
          example: 5500
        status:
          type: string
          enum: [PENDING, PAID, SHIPPED, CANCELLED]
          example: "SHIPPED"
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        shippingAddress:
          $ref: '#/components/schemas/Address'
      required:
        - id
        - customerId
        - createdAt
        - currency
        - totalAmount
        - status
        - items
        - shippingAddress

7.7. Visualizing the Query / Search Model Pattern

sequenceDiagram
    autonumber
    participant C as Client
    participant API as Orders Search API
    participant IDX as Read Index / Search Store

    Note over C,API: Unified search / read model
    C->>API: POST /search-orders<br/>{ filters, sort, limit, cursor }
    API->>IDX: searchOrders(filters, sort, limit, cursor)
    IDX-->>API: [OrderSearchResult...], nextCursor
    API-->>C: 200 OK<br/>{ items: [...], nextCursor }