Category: Design Tags: http-methods, GET, POST, PUT, PATCH, DELETE, idempotence, json-patch, safety
PATCH /resources/{resourceId} over POST /resources/{resourceId}/change-something.410 Gone for subsequent access attempts.| Method | Safe | Idempotent | Has Request Body | Typical Response |
|---|---|---|---|---|
| GET | ✅ Yes | ✅ Yes | No | 200 with body |
| POST | No | No | Yes | 201 (create) / 202 (async) |
| PUT | No | ✅ Yes (must guarantee) | Yes | 200 or 204 |
| PATCH | No | No (unless implemented idempotently) | Yes (JSON Patch) | 200 or 204 |
| DELETE | No | ✅ Yes | No | 204 or 200 |
Use this flowchart to choose the correct HTTP method for a new endpoint:
flowchart TD
Start["Need a new endpoint"] --> Retrieve{"Are you\nretrieving data?"}
Retrieve -- Yes --> SmallQuery{"Can you express the query\nas a small number of\nquery parameters?"}
SmallQuery -- Yes --> GET["GET"]
SmallQuery -- No --> POST_Query["POST\n(query body, return 200)"]
Retrieve -- No --> Mutate{"Are you mutating\nexisting data?"}
Mutate -- Yes --> CanPatch{"Can you use PATCH?\n(partial update)"}
CanPatch -- Yes --> PATCH["PATCH\n(JSON Patch)"]
CanPatch -- No --> EntireResource{"Are you replacing\nthe entire resource?"}
EntireResource -- Yes --> ClientOwns{"Does the client own\nthe resource?\n(API acts as data store)"}
ClientOwns -- Yes --> PUT["PUT"]
ClientOwns -- No --> SeriosulyPatch{"Seriously,\ncan you use PATCH?"}
SeriosulyPatch -- Yes --> PATCH
SeriosulyPatch -- No --> POST_Mutate["POST"]
EntireResource -- No --> POST_Mutate
Mutate -- No --> Creating{"Are you creating\nnew data?"}
Creating -- Yes --> PreferPOST["Prefer POST"]
PreferPOST --> ClientKnowsURL{"Does the client own the resource\nAND already know the URL?"}
ClientKnowsURL -- Yes --> PUT
ClientKnowsURL -- No --> POST_Create["POST"]
Creating -- No --> Deleting{"Are you deleting data\nor removing a resource?"}
Deleting -- Yes --> DELETE["DELETE"]
GET is for retrieving data. It MUST NOT change server state.
GET /customers/123 HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": "123", "name": "Acme Corp" }
Rules:
200 OK.POST is used to create a resource or submit data for processing.
Synchronous creation:
POST /customers HTTP/1.1
Content-Type: application/json
{ "name": "Acme Corp" }
HTTP/1.1 201 Created
Location: /customers/123
Asynchronous operation:
POST /bulk-import HTTP/1.1
Content-Type: application/json
{ ... }
HTTP/1.1 202 Accepted
Location: /bulk-import/status/456
POST for complex queries:
POST /customers/search HTTP/1.1
Content-Type: application/json
{ "filter": { "country": "DE", "status": "active" } }
HTTP/1.1 200 OK
Content-Type: application/json
{ "data": [...] }
PUT replaces an entire resource. Because it replaces the complete resource, it is difficult to evolve APIs that use PUT — adding a new property means existing clients would inadvertently erase it on the next PUT.
Use PUT only when:
PUT /wikis/pages/getting-started HTTP/1.1
Content-Type: application/json
{ "title": "Getting Started", "content": "..." }
Idempotence guarantee examples:
# NOT idempotent — avoid PUT
Given I PUT a Customer
And I publish a CustomerChanged event to external subscribers
Then calling PUT again causes an additional event emission
# Therefore: use POST or PATCH instead
# Idempotent — PUT is acceptable
Given I PUT a Customer
And the customer state is identical to what already exists
And no events are published
Then calling PUT again has no additional effect
PUT and GET consistency: A successful PUT MUST result in a subsequent GET returning an equivalent representation (RFC 7231 §4.3.4).
PATCH is for partial updates. It applies a set of changes to a resource, rather than replacing the whole resource.
PATCH MUST use JSON Patch format (RFC 6902).
PATCH /customers/123 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "/name", "value": "New Name" },
{ "op": "replace", "path": "/address/city", "value": "Berlin" }
]
Why PATCH over POST to an action-resource:
Using POST /customers/123/change-name is an anti-pattern because:
Using PATCH /customers/123 with JSON Patch allows changing any combination of fields in a single request.
JSON Patch operations:
| Op | Description | Example |
|---|---|---|
add |
Add a value | { "op": "add", "path": "/tags/-", "value": "vip" } |
remove |
Remove a value | { "op": "remove", "path": "/nickname" } |
replace |
Replace a value | { "op": "replace", "path": "/name", "value": "Jane" } |
move |
Move a value | { "op": "move", "from": "/old", "path": "/new" } |
copy |
Copy a value | { "op": "copy", "from": "/src", "path": "/dst" } |
test |
Assert a value (for conditional patches) | { "op": "test", "path": "/version", "value": 3 } |
DELETE removes a resource.
DELETE /customers/123 HTTP/1.1
HTTP/1.1 204 No Content
Soft delete consideration: Before exposing DELETE publicly, consider whether a soft delete is more appropriate. After a soft delete, return 410 Gone for subsequent requests to that URI. This allows the resource to be reinstated if needed and provides a better consumer experience than a hard-to-diagnose 404.
| Scenario | Recommended Method |
|---|---|
| Create a resource (server assigns ID) | POST |
| Create a resource (client specifies URL, API is a data store) | PUT |
| Replace an entire resource | PUT (only if idempotent) |
| Update one or more fields of a resource | PATCH (JSON Patch) |
| Trigger an asynchronous operation | POST |
| Query with a complex filter | POST (returns 200) |
| Delete a resource | DELETE |