How do you structure a Web API around commands and queries?
Quick Answer
Queries are safe reads — GET, return a DTO, no side effects. Commands change state — POST/PUT/PATCH/DELETE, often named for the intent (POST /orders/5/cancel) rather than pure CRUD. Internally you can route each to its own handler (lightweight CQRS with MediatR), keeping read and write logic separate.
I split endpoints into queries and commands. Queries are GETs — they return a DTO and don't change anything, and internally the handler just does a projection, no tracking, maybe from a cache. Commands change state, and I often name them for the intent — POST /orders/5/cancel rather than a PATCH that sets a status field — because business operations don't always map to one field. The command handler loads the aggregate, checks the rules, saves in a transaction. Routing each to its own handler, usually with MediatR, keeps both sides small and testable, and it's the natural on-ramp to full CQRS later.
- Pure REST maps CRUD to verbs; task-based / intent endpoints (
/orders/5/approve) express business operations that don't fit a single field update. - Query handler: builds a DTO with a projection, no tracking, maybe hits a read replica or cache.
- Command handler: loads the aggregate, validates invariants, applies the change, saves in a transaction, raises events.
- This separation makes each side simple and independently testable, and is the entry point to full CQRS if the read/write models diverge.