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

Является ли PUT-запрос идемпотентным?

1.7 Middle🔥 181 комментариев
#API и сетевые протоколы

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

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

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

Является ли PUT-запрос идемпотентным?

Да, PUT-запрос идемпотентен по спецификации HTTP.

Что такое идемпотентность?

Идемпотентность — это свойство операции, при котором выполнение её несколько раз даёт такой же результат, что и выполнение один раз.

f(f(x)) = f(x)

Примеры идемпотентных операций:

  • x + 0 = x (идемпотент)
  • x * 1 = x (идемпотент)
  • x + 5 (НЕ идемпотент)

PUT-запрос идемпотентен

По стандарту HTTP PUT полностью заменяет ресурс. Если вызвать его дважды с одинаковыми данными, результат будет идентичен первому вызову.

// Запрос 1: PUT /api/v1/users/123
{
  "name": "John",
  "email": "john@example.com",
  "age": 30
}

// Результат: пользователь с id=123 обновлён
// { id: 123, name: "John", email: "john@example.com", age: 30 }

// Запрос 2: PUT /api/v1/users/123 (ТОТЖЕ запрос)
{
  "name": "John",
  "email": "john@example.com",
  "age": 30
}

// Результат: ИДЕНТИЧЕН запросу 1
// { id: 123, name: "John", email: "john@example.com", age: 30 }

// Запрос 3: PUT /api/v1/users/123 (тот же запрос)
// Результат: СНОВА идентичен
// { id: 123, name: "John", email: "john@example.com", age: 30 }

Пример в Node.js

// users.controller.ts
export class UsersController {
  // PUT — полная замена ресурса (идемпотентен)
  @Put('/users/:id')
  updateUserFully(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    return this.usersService.replace(id, dto);
  }

  // PATCH — частичное обновление (НЕ всегда идемпотентен)
  @Patch('/users/:id')
  updateUserPartially(@Param('id') id: string, @Body() dto: Partial<UpdateUserDto>) {
    return this.usersService.update(id, dto);
  }
}

// users.service.ts
export class UsersService {
  // PUT: полная замена
  replace(id: string, dto: UpdateUserDto) {
    // DELETE старые данные
    // INSERT новые данные
    // Или UPDATE все поля (заменить полностью)
    const user = {
      id,
      name: dto.name,
      email: dto.email,
      age: dto.age,
      // Все остальные поля сбросятся на дефолт или будут заменены
    };
    return this.usersRepository.save(user);
  }

  // PATCH: частичное обновление (может быть неидемпотентным)
  update(id: string, dto: Partial<UpdateUserDto>) {
    // UPDATE поля, которые пришли в dto
    // Остальные поля не трогаем
    return this.usersRepository.update(id, dto);
  }
}

Сравнение: PUT vs PATCH vs POST

PUT — идемпотентен

// Трёхкратный вызов одного и того же PUT
PUT /api/v1/users/123 { name: "John", age: 30 }
PUT /api/v1/users/123 { name: "John", age: 30 }
PUT /api/v1/users/123 { name: "John", age: 30 }

// Результат: 100% идентичен во всех 3 случаях

PATCH — может быть НЕ идемпотентен

// Первый вызов: увеличить age на 1
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 30 + 1 = 31

// Второй вызов: ТОТЖЕ запрос
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 31 + 1 = 32 (ДРУГОЙ результат!)

// Третий вызов
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 32 + 1 = 33 (ещё ДРУГОЙ результат!)

// PATCH НЕ идемпотентен в этом случае!

POST — НЕ идемпотентен

// Первый вызов
POST /api/v1/users { name: "John" }
// Результат: создан пользователь с id=1

// Второй вызов
POST /api/v1/users { name: "John" }
// Результат: создан НОВЫЙ пользователь с id=2

// Третий вызов
POST /api/v1/users { name: "John" }
// Результат: создан НОВЫЙ пользователь с id=3

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

Реальный пример: обновление профиля

// PUT — правильно
router.put('/api/v1/profile/:id', async (req, res) => {
  const { name, email, age, bio } = req.body;
  
  // Заменяем ВСЕ поля профиля
  const profile = await Profile.findByIdAndUpdate(
    req.params.id,
    {
      name,
      email,
      age,
      bio,
      // Если в запросе не пришло поле — оно будет пусто
      // Или используй $set для замены только переданных полей
    },
    { new: true, overwrite: true } // overwrite = полная замена
  );
  
  res.json(profile);
});

// Три идентичных запроса:
PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }

PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }

PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }

// Результат всегда одинаков ✓ ИДЕМПОТЕНТЕН

Почему PUT идемпотентен?

  1. Полная замена — PUT заменяет весь ресурс на новые данные
  2. Без условной логики — не зависит от текущего состояния
  3. Воспроизводимый результат — один и тот же вход = один и тот же выход

Важное замечание: правильная реализация

ПУТ БУДЕТ идемпотентным только если:

// ✓ ПРАВИЛЬНО — полная замена всех полей
router.put('/users/:id', (req, res) => {
  const user = {
    id: req.params.id,
    name: req.body.name,
    email: req.body.email,
    age: req.body.age,
    // Все поля явно переопределены
  };
  return db.save(user);
});

// ✗ НЕПРАВИЛЬНО — добавляет данные
router.put('/users/:id', (req, res) => {
  const user = db.findById(req.params.id);
  user.tags.push(...req.body.newTags); // Добавляем!
  return db.save(user);
  // Второй вызов добавит ещё теги — НЕ идемпотентен!
});

// ✗ НЕПРАВИЛЬНО — изменяет на основе текущего состояния
router.put('/users/:id', (req, res) => {
  const user = db.findById(req.params.id);
  user.age += 1; // Увеличиваем!
  return db.save(user);
  // Второй вызов увеличит возраст ещё раз — НЕ идемпотентен!
});

Практические советы

  1. PUT = полная замена всех полей (или явно заданных полей)
  2. PATCH = частичное обновление (может быть неидемпотентным)
  3. POST = создание нового ресурса (ВСЕГДА неидемпотентен)
  4. Всегда проверяй, что PUT-эндпоинт работает идемпотентно
  5. Документируй, какие поля требуются в PUT запросе
  6. Возвращай 200 OK если ресурс уже в нужном состоянии
  7. Возвращай 201 Created только если создан новый ресурс (в PUT редко)

Заключение

Да, PUT-запрос идемпотентен по спецификации HTTP. Это означает, что отправка одного и того же PUT запроса несколько раз должна дать одинаковый результат. Это сделано для надёжности: если сетевое соединение нестабильно и запрос был отправлен дважды, результат будет корректным. Это ключевое отличие от POST (создание — неидемпотентно) и от PATCH (может быть неидемпотентным).