#15 Spring Boot – POST, PUT czy PATCH?

Tak na prawdę ten artykuł niekoniecznie musi być związany ze Spring Bootem, jednakże posłuży mi on jako realizacja tego zagadnienia. Projektując API REST’owe powinniśmy zwracać uwagę na wiele elementów. Odpowiedni dobór nazw dla endpointów czy korzystanie z kodów odpowiedzi HTTP świadczy o dobrze zaprojektowanym API. Ważnym element jest także korzystanie z metod HTTP do obsługi zasobów. 

1. POST

Metoda POST służy do dodawania nowych elementów. Nowych, czyli takich, których ID jest jeszcze nieznane. Po utworzeniu obiektu należy zwrócić kod 201 “Created”.

Metoda POST:

@PostMapping
@ResponseStatus(value = HttpStatus.CREATED, reason = "Todo created!")
public void addNewTodo(@RequestBody Todo todo){
    todoService.addNewTodo(todo);
}

Test do metody POST:

@Test
public void shouldAddNewTodo() throws Exception {
    // Given
    Todo todo = new Todo("Title", "Description");
    // When
    ResultActions result = this.mvc.perform(post("/todos")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(objectMapper.writeValueAsString(todo)));
    // Then
    result.andExpect(status().isCreated())
            .andExpect(status().reason("Todo created!"));
}

2. PUT

Metoda PUT jest bardzo podobna do metody POST, ponieważ także wysyłamy cały obiekt. Istotną różnicą jest to, że metody PUT używamy wtedy, kiedy ID danego obiektu pochodzi od klienta. Czyli PUT powinien służyć do aktualizowania zasobu. Bardzo ważne jest to, że w przypadku tego mechanizmu podmieniany jest cały obiekt! Po aktualizacji należy zwróć kod 204 “No content”.

Metoda PUT:

@PutMapping("/{id}")
public ResponseEntity<Void> updateTodo(@RequestBody Todo todo, @PathVariable Long id) throws TodoNotFound {
    Todo todoById = todoService.getTodoById(id);
    if(todoById == null){
        todoService.addNewTodo(todo);
        return ResponseEntity.created(URI.create(String.format("/todos/%d", id))).build();
    }
    todoService.update(todo);
    return ResponseEntity.noContent().build();
}

Test do metody PUT:

@Test
public void shouldCreateNewTodoWhenTryToUpdate() throws Exception {
    // Given
    given(todoService.getTodoById(anyLong())).willReturn(null);
    Todo todo = new Todo("New Title", "New Description");
    // When
    ResultActions result = this.mvc.perform(put("/todos/10")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(objectMapper.writeValueAsString(todo)));
    // Then
    result.andExpect(status().isCreated());
}


@Test
public void shouldUpdateTodo() throws Exception {
    // Given
    Todo todo = new Todo(1L, "title", "desc");
    given(todoService.getTodoById(1L)).willReturn(todo);
    // When
    ResultActions result = this.mvc.perform(put("/todos/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(objectMapper.writeValueAsString(todo)));
    // Then
    result.andExpect(status().isNoContent());
}

3. PATCH

O metodzie PUT pisałem, że powinna być wykorzystywana do aktualizacji całego obiektu, ale co w przypadku, gdy chcę zaktualizować tylko część zasobu? Do częściowej aktualizacji służy metoda PATCH. Po wykonaniu operacji należy zwróć kod 204 “No content”.

Metoda PATCH:

@PatchMapping("/{id}")
@ResponseStatus(value = HttpStatus.NO_CONTENT, reason = "Todo partial updated!")
public void updateTodo(@RequestBody Map<String, Object> updates, @PathVariable Long id) throws TodoNotFound {
    Todo todo = todoService.getTodoById(id);
    if (todo == null) {
        throw new TodoNotFound(String.format("Todo with ID %d not exists!", id));
    }
    partialUpdate(todo, updates);
}

private void partialUpdate(Todo todo, Map<String, Object> updates) {
    if (updates.containsKey("title")) {
        todo.setTitle((String) updates.get("title"));
    }
    if (updates.containsKey("description")) {
        todo.setDescription((String) updates.get("description"));
    }
    todoService.partialUpdated(todo);
}

Test do metody PATCH:

@Test
public void shouldReturn404WhenTryPartialUpdateTodoWhichNotExists() throws Exception {
    // Given
    given(todoService.getTodoById(anyLong())).willReturn(null);
    HashMap<String, Object> updates = new HashMap<>();
    updates.put("description", "new description");
    // When
    ResultActions result = this.mvc.perform(patch("/todos/10")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(objectMapper.writeValueAsString(updates)));
    // Then
    result.andExpect(status().isNotFound());
}

@Test
public void shouldPartialUpdateTodo() throws Exception {
    // Given
    Todo todo = new Todo(1L, "title", "desc");
    given(todoService.getTodoById(1L)).willReturn(todo);
    HashMap<String, Object> updates = new HashMap<>();
    updates.put("title", "new title");
    updates.put("description", "new description");
    // When
    ResultActions result = this.mvc.perform(patch("/todos/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(objectMapper.writeValueAsString(updates)));
    // Then
    result.andExpect(status().isNoContent())
            .andExpect(status().reason("Todo partial updated!"));
}

4. Którą wybrać?

Zgodnie z powyższymi akapitami, jeśli dodajemy całkiem nowy zasób to korzystamy z POST. Jeśli chcemy zaktualizować cały element korzystamy z PUT, natomiast PATCH wykorzystujemy do częściowej aktualizacji. Całość na GitHubie.

  • hexin

    Fanie, że są testy 🙂

    • CodeCouple.pl

      Dzięki, jestem fanem i praktykiem TDD także chce zawsze pokazać ,że do wszystkiego da się pisać testy!

  • Rafał Schmidt

    Problem patcha jest dla mnie dość uciążliwy ze względu na to, że nie możemy już używać walidacji JSR w “Map updates” bo nie mamy dto. Jakieś sposoby na rozwiązanie tego?