Небольшой, но законченный API
Пора собрать все части вместе: API пользователей с пятью классическими эндпоинтами —
POST /users создать · GET /users получить список · GET /users/{id} прочитать одного · PUT /users/{id} заменить · DELETE /users/{id} удалить.
Хранилище — это map в памяти, поэтому пример остаётся самодостаточным. (Настоящий сервер обрабатывает запросы параллельно, поэтому в промышленной версии map защищали бы блокировкой и, вероятно, заменили бы базой данных — форма обработчиков при этом не изменилась бы.)
Создание и список
POST декодирует входящий JSON, присваивает пользователю id, сохраняет его и отвечает 201 с созданной записью — вместе с id, чтобы клиент его узнал. GET возвращает всех в виде JSON-массива.
POST /users и GET /users
Два создания, затем список: каждый POST отвечает 201 с сохранённым пользователем, а финальный GET показывает обоих — состояние API росло с каждым запросом.
Вывод: программа на Go и захваченный вывод
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
users := map[int]User{}
nextID := 1
mux := http.NewServeMux()
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, "некорректный JSON", http.StatusBadRequest)
return
}
u.ID = nextID
nextID++
users[u.ID] = u
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
})
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
list := []User{}
for id := 1; id < nextID; id++ {
if u, ok := users[id]; ok {
list = append(list, u)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(list)
})
server := httptest.NewServer(mux)
defer server.Close()
for _, name := range []string{"Ana", "Ben"} {
resp, _ := http.Post(server.URL+"/users", "application/json",
strings.NewReader(`{"name": "`+name+`"}`))
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("POST /users ->", resp.StatusCode, strings.TrimSpace(string(body)))
}
resp, _ := http.Get(server.URL + "/users")
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("GET /users ->", resp.StatusCode, strings.TrimSpace(string(body)))
}
Прочитать одного, заменить, удалить
Маршруты с {id} дополняют набор: чтение отвечает 200 или 404, замена декодирует новые данные в существующий id, удаление отвечает 204. После удаления тот же GET, который раньше работал, честно возвращает 404 — ресурса больше нет.
GET, PUT и DELETE для /users/{id}
Полная жизнь одного ресурса: прочитать его, заменить (ответ несёт новое имя), удалить и не суметь прочитать — каждый шаг с правильным кодом состояния.
Вывод: программа на Go и захваченный вывод
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
users := map[int]User{1: {ID: 1, Name: "Ana"}}
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
w.Header().Set("Content-Type", "application/json")
u, ok := users[id]
if !ok {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, `{"error": "пользователь не найден"}`)
return
}
json.NewEncoder(w).Encode(u)
})
mux.HandleFunc("PUT /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, "некорректный JSON", http.StatusBadRequest)
return
}
u.ID = id
users[id] = u
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(u)
})
mux.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
delete(users, id)
w.WriteHeader(http.StatusNoContent)
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{}
do := func(method, path, payload string) {
var reqBody io.Reader
if payload != "" {
reqBody = strings.NewReader(payload)
}
req, _ := http.NewRequest(method, server.URL+path, reqBody)
resp, _ := client.Do(req)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%-6s %s -> %d %s\n", method, path, resp.StatusCode,
strings.TrimSpace(string(body)))
}
do("GET", "/users/1", "")
do("PUT", "/users/1", `{"name": "Ana Maria"}`)
do("DELETE", "/users/1", "")
do("GET", "/users/1", "")
}