Skip to content

POST Design Guidelines — Creating Resources with Non-Idempotent Operations

DodaTech Updated 2026-06-28 4 min read

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

  1. What status code should a successful POST return?
  2. What header should be included in a successful POST response?
  3. Why is POST not idempotent?
  4. How do you prevent duplicate POST submissions?
  5. How should you handle partial success in bulk creation?

Answers

  1. 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

Why does POST return 201 instead of 200?

201 Created specifically indicates a resource was created. 200 OK is for successful requests that don't create resources.

What is the Location header used for in POST?

It tells the client the URI of the newly created resource.

How do I prevent duplicate POST requests?

Use an Idempotency-Key header that the client sends with each request. The server stores processed keys and returns existing results for duplicates.

Is POST always for creation?

Mostly, but POST is also used for operations that don't fit neatly into other methods, like submitting a search.

What is the 207 Multi-Status response?

A response code for bulk operations that contain both successful and failed items.

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