The Extended CRUD pattern is used when a resource has a lifecycle that goes beyond simple create/read/update/delete.
Instead of overloading PATCH or PUT with magic “status flips,” you expose explicit lifecycle transitions as first-class operations (e.g., submit, approve, decline, publish).
5.1. Overview
Extended CRUD starts with a normal CRUD resource, then adds lifecycle transition endpoints to express the workflow clearly.
Typical examples:
POST /articles/{id}/submitPOST /articles/{id}/approvePOST /articles/{id}/declinePOST /articles/{id}/publish
These operations:
- Express intent (“submit for review”) rather than just “update status”
- Capture workflow rules in the API shape, not only in documentation
- Make authorization, audit, and governance much easier
5.2. When to Use Extended CRUD
Use Extended CRUD when:
-
The resource moves through defined lifecycle states
- e.g.,
DRAFT → SUBMITTED → APPROVED → PUBLISHED
- e.g.,
-
There are business rules about valid transitions
- e.g., you can’t publish without approval
-
Different roles/actors perform different transitions
- Author submits; editor approves; publisher publishes
- You need clear auditability; “who did what and when” matters
- You want to avoid pushing workflow logic into the client or hidden in generic updates
5.3. When NOT to Use Extended CRUD
Avoid Extended CRUD when:
- The resource has no lifecycle, just static data
- There is only one meaningful state (e.g., simple reference data)
- A “status” field is purely informational and doesn’t drive behavior
- The transitions are extremely simple and don’t justify extra endpoints
If the “transition” is actually a business action with broader side-effects (e.g., “cancel order and trigger refunds and notifications”), consider the Functional Resource pattern (Commands) instead of Extended CRUD.
5.4. What the Pattern Looks Like
Extended CRUD keeps the basic CRUD shape and adds explicit transition endpoints.
Base CRUD (simplified)
POST /articles→ create a new article inDRAFTGET /articles/{articleId}→ read current article state
Extended lifecycle transitions
-
POST /articles/{articleId}/submit- DRAFT → SUBMITTED
-
POST /articles/{articleId}/approve- SUBMITTED → APPROVED
-
POST /articles/{articleId}/decline- SUBMITTED → DECLINED (with reason)
-
POST /articles/{articleId}/publish- APPROVED → PUBLISHED
Each transition:
- Has its own endpoint (and sometimes its own request body)
- Can have its own authorization, logging, and metrics
- Makes the workflow visible in the API contract
5.5. Anti-Patterns to Avoid
1. “Status-over-PATCH” instead of transitions
PATCH /articles/{id}
{
"status": "PUBLISHED"
}
- Hides which transitions are valid
- Makes authorization coarse-grained (“can edit status”)
- Makes audit logs less clear (“status changed” vs “publish requested/approved”)
2. Single “transition” endpoint with action field
POST /articles/{id}/transition
{
"action": "SUBMIT"
}
- Recreates a command bus inside one endpoint
- Loses the clarity, discoverability, and per-action governance benefits
3. Over-exploding endpoints
Creating endpoints for trivial, reversible, low-value changes (e.g., /articles/{id}/highlight) may create noise. Reserve Extended CRUD for business-significant lifecycle steps.
5.6. OpenAPI Example
A lean but complete OpenAPI 3.0.3 document showing:
POST /articlesandGET /articles/{articleId}(base CRUD)- Extended endpoints:
submit,approve,decline,publish - Minimal schemas focused on status transitions and a decline reason
openapi: 3.0.3
info:
title: Articles API - Extended CRUD Pattern Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/articles:
post:
summary: Create a new article
tags: [Articles]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleCreateRequest'
examples:
default:
value:
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
responses:
'201':
description: Created
headers:
Location:
schema:
type: string
example: https://api.example.com/articles/ar_12345
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: DRAFT
/articles/{articleId}:
get:
summary: Get an article by ID
tags: [Articles]
parameters:
- in: path
name: articleId
required: true
schema:
type: string
example: ar_12345
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: SUBMITTED
'404':
description: Not Found
/articles/{articleId}/submit:
post:
summary: Submit an article for review
tags: [Articles]
parameters:
- in: path
name: articleId
required: true
schema:
type: string
example: ar_12345
responses:
'200':
description: Submitted
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: SUBMITTED
'409':
description: Invalid state to submit (e.g., already published)
'404':
description: Not Found
/articles/{articleId}/approve:
post:
summary: Approve an article
tags: [Articles]
parameters:
- in: path
name: articleId
required: true
schema:
type: string
example: ar_12345
responses:
'200':
description: Approved
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: APPROVED
'409':
description: Invalid state to approve
'404':
description: Not Found
/articles/{articleId}/decline:
post:
summary: Decline an article
tags: [Articles]
parameters:
- in: path
name: articleId
required: true
schema:
type: string
example: ar_12345
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DeclineArticleRequest'
examples:
default:
value:
reason: "Article lacks sufficient technical depth."
responses:
'200':
description: Declined
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: DECLINED
'409':
description: Invalid state to decline
'404':
description: Not Found
/articles/{articleId}/publish:
post:
summary: Publish an article
tags: [Articles]
parameters:
- in: path
name: articleId
required: true
schema:
type: string
example: ar_12345
responses:
'200':
description: Published
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
examples:
default:
value:
id: ar_12345
title: "How to Design APIs with Extended CRUD"
body: "Extended CRUD helps model workflows as lifecycle transitions..."
status: PUBLISHED
'409':
description: Invalid state to publish
'404':
description: Not Found
components:
schemas:
Article:
type: object
properties:
id:
type: string
example: ar_12345
title:
type: string
example: "How to Design APIs with Extended CRUD"
body:
type: string
example: "Extended CRUD helps model workflows as lifecycle transitions..."
status:
type: string
enum: [DRAFT, SUBMITTED, APPROVED, DECLINED, PUBLISHED]
example: DRAFT
required:
- id
- title
- body
- status
ArticleCreateRequest:
type: object
properties:
title:
type: string
example: "How to Design APIs with Extended CRUD"
body:
type: string
example: "Extended CRUD helps model workflows as lifecycle transitions..."
required:
- title
- body
DeclineArticleRequest:
type: object
properties:
reason:
type: string
example: "Article lacks sufficient technical depth."
required:
- reason
This example keeps the focus strictly on the lifecycle: you can show teams how Extended CRUD cleanly separates lifecycle transitions from generic updates.
5.7. Visualizing Extended CRUD (Mermaid Diagram)
For Extended CRUD, a state diagram is usually more expressive than a simple sequence diagram, because the pattern is all about lifecycle transitions.
stateDiagram-v2
[*] --> DRAFT
DRAFT --> SUBMITTED: POST /articles/{id}/submit
SUBMITTED --> APPROVED: POST /articles/{id}/approve
SUBMITTED --> DECLINED: POST /articles/{id}/decline
APPROVED --> PUBLISHED: POST /articles/{id}/publish
DRAFT: status = "DRAFT"
SUBMITTED: status = "SUBMITTED"
APPROVED: status = "APPROVED"
DECLINED: status = "DECLINED"
PUBLISHED: status = "PUBLISHED"