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 POST request signals an intent to change the state of the system by creating a new resource. The response would typically be a 201 Created with a Location header 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 PUT request, targeting a specific resource, signifies a complete replacement or update of that resource’s state. A 200 OK or 204 No Content is common.

  • Delete Product: A client wants to remove a product.

    DELETE /products/12345
    

    The DELETE verb clearly maps to a command that removes data. A 204 No Content is typical.

Queries:

  • Get Product Details: A client wants to view a specific product.

    GET /products/12345
    

    This GET request is a pure query. It requests data without altering the system’s state. A 200 OK with 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=10
    

    This GET request retrieves a collection. Query parameters (category, sort, page, pageSize) are used to refine the query, not to change data. The response would be a 200 OK with 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.

Want structured learning?

Take the full Cqrs course →