← Назад к вопросам

Что из этих трех идемпотентно: POST, PUT и PATCH?

1.8 Middle🔥 181 комментариев
#REST API и HTTP

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Идемпотентность методов HTTP: POST, PUT, PATCH

Определение идемпотентности

Идемпотентный метод - это метод, при повторном вызове с теми же параметрами на сервере происходит ТО ЖЕ СОСТОЯНИЕ, что и при первом вызове. Боковых эффектов нет.

Формально: f(f(x)) = f(x)

Идемпотентный метод:
Первый вызов:  x = 1
Второй вызов:  x = 1  (то же самое)
Третий вызов:  x = 1  (то же самое)

Неидемпотентный метод:
Первый вызов:  счет = 1000, вычитаем 100 → счет = 900
Второй вызов:  счет = 900, вычитаем 100 → счет = 800  (другое состояние!)

Краткий ответ

МетодИдемпотентныйПочему
POST❌ НЕТСоздает новый ресурс каждый раз
PUT✅ ДАЗаменяет ресурс полностью
PATCH❌ НЕТМожет добавлять/изменять данные

Детальное объяснение

POST - НЕИДЕМПОТЕНТНЫЙ

POST /api/users
{
  "name": "Alice",
  "email": "alice@example.com"
}

Первый запрос:

  • Создается пользователь с id=1
  • Ответ: 201 Created

Второй идентичный запрос:

  • Создается НОВЫЙ пользователь с id=2
  • Ответ: 201 Created

Третий запрос:

  • Создается НОВЫЙ пользователь с id=3

POST НЕ идемпотентный - каждый вызов создает новый ресурс!

# FastAPI пример
@app.post("/users")
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    new_user = User(name=user.name, email=user.email)
    db.add(new_user)
    db.commit()
    return new_user

# Каждый вызов создает нового пользователя

PUT - ИДЕМПОТЕНТНЫЙ

PUT /api/users/1
{
  "name": "Alice",
  "email": "alice.new@example.com",
  "age": 30
}

Первый запрос:

  • Пользователь с id=1 получает все эти данные
  • Ответ: 200 OK или 204 No Content

Второй идентичный запрос:

  • Пользователь с id=1 получает ТЕ ЖЕ данные
  • Ответ: 200 OK или 204 No Content

Третий запрос:

  • Пользователь с id=1 получает ТЕ ЖЕ данные

PUT ИДЕМПОТЕНТНЫЙ - состояние ресурса после каждого вызова одинаковое!

# FastAPI пример
@app.put("/users/{user_id}")
def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if not db_user:
        raise HTTPException(status_code=404)
    
    # Заменяем все поля полностью
    db_user.name = user.name
    db_user.email = user.email
    db_user.age = user.age
    db.commit()
    
    return db_user

# Повторные вызовы с теми же данными не меняют состояние

PATCH - НЕИДЕМПОТЕНТНЫЙ (в большинстве случаев)

PATCH /api/users/1
{
  "age": 31
}

Первый запрос:

  • Возраст пользователя 1: 30 → 31

Второй идентичный запрос:

  • Возраст пользователя 1: 31 → 31 (совпадает, кажется идемпотентным)

Но если операция - инкремент:

PATCH /api/users/1
{
  "age_increment": 1
}

Первый запрос:

  • Возраст: 30 → 31

Второй запрос:

  • Возраст: 31 → 32

PATCH НЕ идемпотентный при изменении данных (особенно при операциях типа инкремента)!

# FastAPI пример с PATCH
@app.patch("/users/{user_id}")
def partial_update_user(user_id: int, updates: dict, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.id == user_id).first()
    if not db_user:
        raise HTTPException(status_code=404)
    
    # Обновляем только переданные поля
    for field, value in updates.items():
        if hasattr(db_user, field):
            setattr(db_user, field, value)
    
    db.commit()
    return db_user

# PATCH /users/1 с {"age_increment": 1}
# Первый: age 30 → 31
# Второй: age 31 → 32 (ДРУГОЕ состояние!)

Сравнение POST, PUT, PATCH

МЕТОД | ИДЕМПОТЕНТНЫЙ | ТЕЛО ЗАПРОСА | ДЕЙСТВИЕ
------|---------------|--------------|---------------------------
POST  | ❌             | Полное       | Создает новый ресурс
PUT   | ✅             | Полное       | Заменяет ресурс полностью
PATCH | ❌             | Частичное    | Обновляет часть ресурса

Реальные примеры

Сценарий: Обновление профиля пользователя

POST - неправильно

POST /api/users
{
  "name": "Alice",
  "email": "alice@example.com"
}

Первый запрос: создает user_id=1
Второй запрос: создает user_id=2  ❌ (новый ресурс)

PUT - правильно

PUT /api/users/1
{
  "name": "Alice",
  "email": "alice@example.com",
  "age": 30,
  "city": "NYC"
}

Первый запрос: user_id=1 имеет эти данные
Второй запрос: user_id=1 имеет ТЕ ЖЕ данные ✅

PATCH - может работать, но опасно

PATCH /api/users/1
{
  "age": 30
}

Первый запрос: age = 30 ✅
Второй запрос: age = 30 ✅ (совпадает)
Но если будет инкремент - не идемпотентно!

Пример с обработкой платежей

# ❌ Неправильно - POST с каждым вызовом создает платеж
@app.post("/payments")
def create_payment(payment: Payment):
    # Первый вызов: создает платеж #1, снимает $100
    # Второй вызов: создает платеж #2, снимает еще $100
    # Третий вызов: создает платеж #3, снимает еще $100
    return {"payment_id": create_new_payment(payment)}

# ✅ Правильно - используем идемпотентный ключ с PUT
@app.put("/payments/{idempotency_key}")
def create_payment(idempotency_key: str, payment: Payment):
    # Проверяем, есть ли платеж с таким ключом
    existing = db.query(Payment).filter(
        Payment.idempotency_key == idempotency_key
    ).first()
    
    if existing:
        # Возвращаем существующий платеж
        return existing
    
    # Создаем новый платеж только если его нет
    new_payment = Payment(idempotency_key=idempotency_key, ...)
    db.add(new_payment)
    db.commit()
    return new_payment

# PUT /payments/request-123 → платеж создан
# PUT /payments/request-123 → возвращает тот же платеж

REST рекомендации

ОперацияМетодИдемпотентныйПример
СозданиеPOSTPOST /users
ЧтениеGETGET /users/1
Обновление всегоPUTPUT /users/1
Обновление частиPATCHPATCH /users/1
УдалениеDELETEDELETE /users/1

Лучшие практики

1. POST для создания

@app.post("/users")
def create_user(user: UserCreate):
    return create_new_user(user)

2. PUT для полного обновления

@app.put("/users/{user_id}")
def update_user(user_id: int, user: UserUpdate):
    # Заменяем ВСЕ поля
    return replace_user(user_id, user)

3. PATCH для частичного обновления

@app.patch("/users/{user_id}")
def partial_update(user_id: int, updates: dict):
    # Обновляем ТОЛЬКО переданные поля
    return update_user_fields(user_id, updates)

4. Используй идемпотентные ключи для критичных операций

# Заголовок Idempotency-Key для платежей
@app.post("/payments")
def create_payment(payment: Payment, idempotency_key: str = Header()):
    cached = cache.get(f"payment:{idempotency_key}")
    if cached:
        return cached
    
    result = process_payment(payment)
    cache.set(f"payment:{idempotency_key}", result, ttl=3600)
    return result

Вывод

PUT идемпотентный, POST и PATCH неидемпотентные.

  • POST: Создает новый ресурс каждый раз → ❌ Не идемпотентный
  • PUT: Заменяет ресурс на те же данные → ✅ Идемпотентный
  • PATCH: Обновляет часть ресурса → ❌ Не идемпотентный

Используй PUT для операций, которые должны быть безопасны при повторе. Для критичных операций (платежи) используй идемпотентные ключи.

Что из этих трех идемпотентно: POST, PUT и PATCH? | PrepBro