Говорите точно то, что имеете в виду
REST API общается на двух языках. Метод говорит, чего хочет клиент: GET — прочитать, POST — создать, PUT — заменить, PATCH — изменить часть, DELETE — удалить. Код состояния говорит, чем всё закончилось: 2xx — успех, 4xx — ошибка клиента, 5xx — ошибка сервера.
Одна деталь, которую стоит знать: PUT идемпотентен — отправить его дважды означает то же самое, что отправить один раз. POST — нет: два POST создадут два ресурса.
Правильный метод
Шаблоны с методами в mux направляют каждый глагол своему обработчику. Создание отвечает 201 Created и указывает на новый ресурс заголовком Location; удаление отвечает 204 No Content — успех, о котором нечего сказать.
GET, POST и DELETE для одного ресурса
Три глагола, три ответа: список возвращается с кодом 200, создание — с кодом 201 и заголовком Location, удаление — с кодом 204 и пустым телом.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /notes", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "заметки: покупки, идеи")
})
mux.HandleFunc("POST /notes", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/notes/3")
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "создано")
})
mux.HandleFunc("DELETE /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{}
get, _ := http.Get(server.URL + "/notes")
body, _ := io.ReadAll(get.Body)
fmt.Println("GET /notes ->", get.StatusCode, strings.TrimSpace(string(body)))
post, _ := http.Post(server.URL+"/notes", "application/json", strings.NewReader(`{"text": "позвонить Ben"}`))
fmt.Println("POST /notes ->", post.StatusCode, "Location:", post.Header.Get("Location"))
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/notes/3", nil)
del, _ := client.Do(req)
fmt.Println("DELETE /notes/3 ->", del.StatusCode, "(нет содержимого)")
}
Правильный код состояния
Ошибки тоже заслуживают точности: отсутствующий ресурс — это 404 Not Found (с телом ошибки в формате JSON, чтобы его могли прочитать программы), а неверный глагол на известном пути — 405 Method Not Allowed, который mux в Go отвечает за вас, когда путь совпадает, а метод — нет.
200, 404 и 405
Существующая заметка возвращает 200 с данными, отсутствующая — 404 с ошибкой в формате JSON, а DELETE на маршруте только для GET отклоняется самим маршрутизатором с кодом 405.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
notes := map[string]string{"1": "список покупок"}
mux := http.NewServeMux()
mux.HandleFunc("GET /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
text, ok := notes[r.PathValue("id")]
if !ok {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, `{"error": "заметка не найдена"}`)
return
}
fmt.Fprintf(w, `{"id": %q, "text": %q}`+"\n", r.PathValue("id"), text)
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{}
for _, path := range []string{"/notes/1", "/notes/99"} {
resp, _ := http.Get(server.URL + path)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("GET ", path, "->", resp.StatusCode, strings.TrimSpace(string(body)))
}
// Неверный метод на известном пути: маршрутизатор сам отвечает 405.
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/notes/1", nil)
resp, _ := client.Do(req)
fmt.Println("DELETE /notes/1 ->", resp.StatusCode, http.StatusText(resp.StatusCode))
}