REST APIs are a natural fit for Command Query Responsibility Segregation (CQRS), but mapping them requires understanding that not all REST verbs map cleanly to one or the other.
Let’s see it in action. Imagine a simple e-commerce system.
Commands:
-
Create Product: A client wants to add a new product.
POST /products Content-Type: application/json { "name": "Wireless Mouse", "description": "Ergonomic wireless mouse", "price": 25.99 }This
POSTrequest signals an intent to change the state of the system by creating a new resource. The response would typically be a201 Createdwith aLocationheader pointing to the new resource:HTTP/1.1 201 Created Location: /products/12345 Content-Type: application/json { "productId": "12345", "name": "Wireless Mouse", "description": "Ergonomic wireless mouse", "price": 25.99 } -
Update Product: A client wants to modify an existing product.
PUT /products/12345 Content-Type: application/json { "name": "Advanced Wireless Mouse", "description": "Ergonomic wireless mouse with extra buttons", "price": 29.99 }This
PUTrequest, targeting a specific resource, signifies a complete replacement or update of that resource’s state. A200 OKor204 No Contentis common. -
Delete Product: A client wants to remove a product.
DELETE /products/12345The
DELETEverb clearly maps to a command that removes data. A204 No Contentis typical.
Queries:
-
Get Product Details: A client wants to view a specific product.
GET /products/12345This
GETrequest is a pure query. It requests data without altering the system’s state. A200 OKwith the product data is expected. -
List Products: A client wants to see a collection of products, potentially with filtering and pagination.
GET /products?category=electronics&sort=price_asc&page=2&pageSize=10This
GETrequest retrieves a collection. Query parameters (category,sort,page,pageSize) are used to refine the query, not to change data. The response would be a200 OKwith a list of products.
The core problem CQRS solves is the impedance mismatch between the highly optimized, often different, models required for writing (commands) and reading (queries). In a traditional CRUD API, a single model is often shoehorned to fit both. With CQRS, you can have a lean, fast write model optimized for persistence and a denormalized, highly queryable read model optimized for retrieval. The REST API then becomes the facade, exposing these distinct operations. POST, PUT, DELETE typically map to commands, while GET maps to queries.
It’s crucial to remember that RESTful commands don’t have to be limited to the "safe" HTTP methods. While GET is always a query, POST can be used for actions that aren’t strictly "create" but rather "perform an action." For instance, initiating a complex workflow or processing a batch of items might use POST /orders/{orderId}/process. The key is that the intent of the request, regardless of HTTP method, dictates whether it’s a command (state-changing) or a query (state-retrieving).
A common pitfall is trying to force all query operations into a single endpoint with an overwhelming number of parameters. Instead, consider creating distinct query endpoints or using a GraphQL-like approach for complex read scenarios if the REST API becomes unwieldy.
The next hurdle is handling the eventual consistency between your write and read models.