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-ordersPOST /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
GETwith query params for simple, non-sensitive filters. UsePOSTwith 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 aSearchOrdersRequestwith filters and pagination, returnsOrderSearchResultprojections.
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 /resourceis 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
OrderSearchResultprojections +nextCursor
And, alongside it:
-
GET /orders/{orderId}- Returns full
OrderDetailfor a specific order
- Returns full
The contrast:
POST /search-orders→ search/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 }