POST Design Guidelines — Creating Resources with Non-Idempotent Operations
In this tutorial, you will learn about POST Design Guidelines. We cover key concepts, practical examples, and best practices to help you master this topic.
POST is used in REST APIs primarily for creating new resources. Unlike PUT, POST is not idempotent, meaning multiple identical POST requests create multiple resources, each with a unique identifier.
What You'll Learn
- How to design POST endpoints for resource creation
- What status codes and headers to return
- How to handle validation and duplicate prevention
Why It Matters
POST is the most complex HTTP method because it can have side effects. Proper POST design prevents duplicate resources, communicates errors clearly, and provides clients with everything they need to access the newly created resource.
Real-World Use
DodaTech's order creation endpoint receives POST requests with order details. Each request creates a unique order with a new ID. The response includes a 201 status, Location header pointing to the new order, and the complete order representation. An idempotency key prevents duplicate orders from network retries.
sequenceDiagram
Client->>API: POST /api/orders
Note over Client: Body: order details
API->>Database: Insert order
Database-->>API: Order ID: 567
API-->>Client: 201 Created
Note over Client: Location: /api/orders/567
Note over Client: Body: order data
POST Implementation
from flask import Flask, jsonify, request, url_for
import uuid
app = Flask(__name__)
@app.route('/api/orders', methods=['POST'])
def create_order():
data = request.get_json()
if not data:
return jsonify({
"error": "invalid_json",
"message": "Request body must be valid JSON"
}), 400
# Validate required fields
required_fields = ['customer_id', 'items', 'shipping_address']
missing = [f for f in required_fields if f not in data]
if missing:
return jsonify({
"error": "validation_error",
"message": f"Missing required fields: {', '.join(missing)}"
}), 400
# Validate items array
if not isinstance(data['items'], list) or len(data['items']) == 0:
return jsonify({
"error": "validation_error",
"message": "Items must be a non-empty array"
}), 400
# Create the order
order = database.create_order({
'customer_id': data['customer_id'],
'items': data['items'],
'shipping_address': data['shipping_address'],
'notes': data.get('notes', ''),
'status': 'pending'
})
response = jsonify(order.to_dict())
response.status_code = 201
response.headers['Location'] = f"/api/orders/{order.id}"
return response
Idempotency for POST
@app.route('/api/payments', methods=['POST'])
def create_payment():
idempotency_key = request.headers.get('Idempotency-Key')
if not idempotency_key:
return jsonify({
"error": "missing_idempotency_key",
"message": "Idempotency-Key header is required for payment requests"
}), 400
# Check if this key was already processed
existing = database.get_payment_by_idempotency_key(idempotency_key)
if existing:
# Return the existing payment without creating a duplicate
return jsonify(existing.to_dict()), 200
data = request.get_json()
payment = database.create_payment(data)
database.store_idempotency_key(idempotency_key, payment.id)
response = jsonify(payment.to_dict())
response.status_code = 201
response.headers['Location'] = f"/api/payments/{payment.id}"
response.headers['Idempotency-Key'] = idempotency_key
return response
Bulk Creation
@app.route('/api/products/bulk', methods=['POST'])
def bulk_create_products():
data = request.get_json()
if not isinstance(data, list):
return jsonify({
"error": "invalid_format",
"message": "Request body must be an array of products"
}), 400
results = []
errors = []
for i, product_data in enumerate(data):
if 'name' not in product_data:
errors.append({"index": i, "error": "name_required"})
continue
try:
product = database.create_product(product_data)
results.append(product.to_dict())
except Exception as e:
errors.append({"index": i, "error": str(e)})
response_data = {
"created": results,
"errors": errors,
"success_count": len(results),
"error_count": len(errors)
}
status_code = 201 if len(errors) == 0 else 207
response = jsonify(response_data)
response.status_code = status_code
return response
Common Mistakes
1. Returning 200 Instead of 201
Successful resource creation should return 201 Created. Returning 200 makes it harder for clients to distinguish creation from retrieval.
2. Omitting the Location Header
The Location header tells the client where to find the new resource. Forgetting it forces clients to construct URLs from response data.
3. Not Validating Before Inserting
Validate all input data before creating the resource. Database-level errors should be rare if validation is thorough.
4. Exposing Internal IDs in Error Messages
Error messages like "Duplicate entry 123 for key 'orders.PRIMARY'" leak implementation details. Use user-friendly error messages.
5. Not Handling Duplicate Submissions
Network retries cause duplicate POST requests. Use idempotency keys or unique constraints to prevent duplicate resources.
Practice Questions
- What status code should a successful POST return?
- What header should be included in a successful POST response?
- Why is POST not idempotent?
- How do you prevent duplicate POST submissions?
- How should you handle partial success in bulk creation?
Answers
- 201 Created. 2. Location header with the URI of the created resource. 3. Because each POST creates a new resource, repeating the same POST creates multiple resources. 4. Use Idempotency-Key headers or unique constraints. 5. Return 207 Multi-Status with per-item results and errors.
Challenge
Build an order creation API with: comprehensive input validation, idempotency support via Idempotency-Key header, 201 + Location header on success, proper error responses for validation failures, and a bulk creation endpoint with partial success handling.
FAQ
Mini Project
Build a comprehensive POST handling API for a task management system: single task creation with validation, bulk task creation with partial success, file upload via POST multipart/form-data, and idempotent payment processing with Idempotency-Key headers.
What's Next
- Learn about PUT vs PATCH for full vs partial resource updates
- Explore JSON Patch (RFC 6902) for API resource patching
- Continue to idempotency guarantees for PUT and DELETE
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro