REST APIs are often thought of as representing the current state of a resource, but they can also be used to change that state, and the distinction is surprisingly important for how you design them.

Let’s see how this looks in practice. Imagine a simple e-commerce system.

Scenario: Placing an Order

A customer wants to place an order. This is a command – an action that changes the system’s state.

POST /orders
Host: api.example.com
Content-Type: application/json

{
  "customerId": "customer-123",
  "items": [
    {"productId": "product-abc", "quantity": 2},
    {"productId": "product-xyz", "quantity": 1}
  ],
  "shippingAddress": {
    "street": "123 Main St",
    "city": "Anytown",
    "zip": "12345"
  }
}

The POST method is used here because we are creating a new resource (/orders). The request body contains all the information needed to fulfill this command.

The API might respond with:

201 Created
Location: /orders/order-789
Content-Type: application/json

{
  "orderId": "order-789",
  "status": "PENDING_PAYMENT",
  "message": "Order created successfully. Please proceed to payment."
}

This 201 Created status code is standard for successful POST requests that result in resource creation. The Location header tells us where to find the newly created order.

Scenario: Checking Order Status

Now, let’s say the customer wants to check the status of their order. This is a query – an action that retrieves information without changing the system’s state.

GET /orders/order-789
Host: api.example.com

The GET method is idempotent and safe, making it perfect for queries. We’re simply asking for the current representation of the /orders/order-789 resource.

The API might respond with:

200 OK
Content-Type: application/json

{
  "orderId": "order-789",
  "customerId": "customer-123",
  "status": "SHIPPED",
  "items": [
    {"productId": "product-abc", "quantity": 2},
    {"productId": "product-xyz", "quantity": 1}
  ],
  "shippingAddress": {
    "street": "123 Main St",
    "city": "Anytown",
    "zip": "12345"
  },
  "trackingNumber": "TRK123456789"
}

This 200 OK status code indicates a successful retrieval of the order details.

The core problem this design addresses is the ambiguity that arises when a single API endpoint is responsible for both actions (commands) and information retrieval (queries). By separating these concerns, we leverage the well-understood semantics of HTTP methods and resource URIs. Commands, which represent intent and often change state, are naturally mapped to methods like POST, PUT, and DELETE. Queries, which are about retrieving state, align perfectly with GET. This separation makes APIs more predictable, easier to understand, and more robust. It also allows for different underlying implementations: commands might trigger complex business logic and asynchronous processing, while queries can be optimized for read performance and served from cached data.

When you’re designing commands, especially those that are meant to be retried, consider the idempotency implications of the HTTP method you choose. While POST is generally not idempotent, if your command represents a "create or update if not exists" operation, you might find yourself returning a 200 OK with the existing resource details even if the request was a duplicate, effectively making the effect idempotent. However, for pure commands that must be executed, relying on POST and handling potential duplicates at the application level (e.g., with unique idempotency keys in the request headers) is often the cleaner approach.

The next challenge is handling complex queries that require filtering or pagination.

Want structured learning?

Take the full Cqrs course →