HTTP-сервер, JSON API и итоговый проект

JSON HTTP API: Task API с валидацией, единообразными ошибками и потокобезопасным хранилищем

Содержание курса

Switch по r.Method, ответ 405 и маршрутизация /tasks и /tasks/{id}

Регистрируем два паттерна:

mux := http.NewServeMux()
mux.HandleFunc("/tasks", store.handleTasks)
mux.HandleFunc("/tasks/", store.handleTaskByID)

Паттерн /tasks без слеша на конце матчит только точный путь /tasks. Паттерн /tasks/ с завершающим слешем — это префикс, он поймает /tasks/1, /tasks/42 и всё остальное, начинающееся с /tasks/. Так стандартный ServeMux разграничивает «список» и «конкретный элемент».

Внутри каждого обработчика читаем r.Method:

func (s *Store) handleTasks(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        s.listTasks(w, r)
    case http.MethodPost:
        s.createTask(w, r)
    default:
        writeError(w, http.StatusMethodNotAllowed, "method not allowed")
    }
}

Почему не положиться на ServeMux с method-qualified паттернами вроде "GET /tasks"? Потому что когда ServeMux сам формирует 405, он отдаёт текстовый ответ без гарантии application/json и нашего поля error. Клиент нашего API ожидает единый формат ошибок — и default-ветка с writeError это обеспечивает. Собственный switch выбран не из-за ограничений Go, а ради контракта.

Для /tasks/ нужно ещё извлечь ID из пути:

func (s *Store) handleTaskByID(w http.ResponseWriter, r *http.Request) {
    idStr := strings.TrimPrefix(r.URL.Path, "/tasks/")
    id, err := strconv.Atoi(idStr)
    if err != nil || id <= 0 {
        writeError(w, http.StatusNotFound, "task not found")
        return
    }
    switch r.Method {
    case http.MethodPatch:
        s.updateTask(w, r, id)
    case http.MethodDelete:
        s.deleteTask(w, r, id)
    default:
        writeError(w, http.StatusMethodNotAllowed, "method not allowed")
    }
}

strings.TrimPrefix отрезает /tasks/ — остаётся строка с числом. Если Atoi возвращает ошибку или число не положительное, это уже некорректный запрос, и 404 здесь уместнее 400: такого ресурса не существует.