Простой CRUD на chi. Часть 1
- понедельник, 18 марта 2024 г. в 00:00:17
Согласно официальному сайту, chi — это легковесный, идиоматический и композируемый маршрутизатор для создания HTTP-сервисов на Go. Он на 100% совместим с net/http
и довольно легок в обращении, однако его документация предназначена скорее для опытных разработчиков, чем для новичков, поэтому я решил написать серию статей, в ходе которых мы будем постепенно развивать и перерабатывать простейший CRUD, написанный на chi.
В рамках данной части мы напишем код, который ляжет в основу дальнейших статей. Это будет простой и в чем-то даже грязный код, но это сделано умышленно, чтобы автор вместе с читателем мог прогрессировать от части к части. Код для каждой последующей части будет появляться по мере написания в этом репозитории и помещаться в отдельную ветку, а весь код, написанный для этой части, находится в этой ветке.
Наш CRUD будет обслуживать хранение и обработку следующей структуры:
type CrudItem struct {
Id int
Name string
Description string
internal string
}
За хранение записей будут отвечать следующие две переменные:
currentId := 1
storage := make(map[int]CrudItem)
Сущности мы будем сохранять в карте/словаре (вам как больше нравится?). При необходимости добавить значение в хранилище, оно добавляется по ключу currentId
. Я хочу подчеркнуть, что это решение с запахом и не предназначено для использования в реальных проектах. В следующих частях мы отрефакторим механизм хранения, вынесем его за интерфейс и сделаем его потокобезопасным (но не сегодня).
Простейшая программа с использованием chi будет выглядеть так:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
http.ListenAndServe(":3000", r)
}
Она ничего не делает, кроме создания структуры маршрутизатора и запуска его обслуживания на трехтысячном порту.
Создание простейшего обработчика и навешивание его на паттерн пути в chi выглядит следующим образом:
Выбрать метод марштрутизатора, соответствующий необходимому HTTP-методу
Передать в него паттерн пути и обработчик http.HandlerFunc
(функция с сигнатурой func(w http.ResponseWriter, r *http.Request)
). Из коробки нам доступны следующие HTTP-методы:
Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
Этого достаточно для написания стандартного CRUD-а, но если вам необходимо написать обработчик собственного кастомного HTTP-метода, то вам сначала необходимо зарегистрировать его с помощью chi.RegisterMethod("JELLO")
, а затем навесить на паттерн пути в маршрутизаторе обработчик с помощью r.Method("JELLO", "/path", myJelloMethodHandler)
.
Код регистрации обработчика для добавления нового CrudItem
в наше импровизированное хранилище выглядит следующим образом:
r.Post("/crud-items/", func(w http.ResponseWriter, r *http.Request) {
var item CrudItem
err := json.NewDecoder(r.Body).Decode(&item)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
item.Id = currentId
storage[currentId] = item
jsonItem, err := json.Marshal(item)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(jsonItem)
currentId += 1
})
Что из себя представляет наша реализация обработчика:
Пытаемся прочитать из тела запроса json и десериализовать его в структуру CrudItem
. Валидный JSON выглядит так:
{
"name": "New name",
"description": "New description"
}
Если по какой-то причине нам не удалось это сделать, мы говорим пользователю о том, что с его запросом что-то не так и заканчиваем работу.
Присваиваем сущности Id
и сохраняем в наше хранилище. Ходят легенды, что в хороших CRUD-ах принято возвращать добавленный объект с присвоенными ему идентификаторами, и мы поступаем так же:
Сериализуем структуру CrudItem
в json;
В случае провала говорим пользователю, что что-то пошло не так по нашей вине;
В случае успеха отправляем пользователю json и инкрементим текущий Id
.
Чтение мы сделаем двумя обработчиками:
Прочитать все записи;
Прочитать конкретную запись. Ниже приведен обработчик для получения всех сохраненных записей, но пока он нам не интересен -- он нужен нам для следующих частей:
r.Get("/crud-items/", func(w http.ResponseWriter, r *http.Request) {
result := make([]CrudItem, 0, len(storage))
for _, item := range storage {
result = append(result, item)
}
resultJson, err := json.Marshal(result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(resultJson)
})
Гораздо интереснее выглядит обработчик получения записи по Id
:
r.Get("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
resultJson, err := json.Marshal(storage[id])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(resultJson)
})
Здесь мы воспользовались получением id
записи из URL. Для этого мы:
Задали в паттерне пути именной параметр id
с помощью {id}
;
С помощью chi.URLParam(r, "id")
получили строковое значение параметра id
;
Попробовали привести параметр id
к целому числу и в случае провала сообщили пользователю, что с его запросом что-то не так.
Объединив реализации обработчика для добавления новой записи и получения записи по id
мы можем соорудить обработчик для обновления записи:
r.Put("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
var item CrudItem
err = json.NewDecoder(r.Body).Decode(&item)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
item.Id = id
storage[id] = item
jsonItem, err := json.Marshal(item)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Write(jsonItem)
})
Удаление записи из нашего хранилища выглядит следующим образом:
r.Delete("/crud-items/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.Atoi(idStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if _, ok := storage[id]; !ok {
w.WriteHeader(http.StatusNotFound)
return
}
delete(storage, id)
})
По сути, удаление записи это удаление элемента словаря с предварительной проверкой наличия элемента.
На этом создание базового приложения заканчивается. Сегодня мы реализовали CRUD с 5-ю обработчиками, используя маршрутизатор chi, научились читать json из тела запроса, отправлять его в ответ и получать значение из паттерна пути.
Чему будут посвящены следующие статьи:
Рефакторинг хранилища и вынос его за интерфейс;
Пагинация для обработчика получения всех записей с использованием middleware;
Использование интерфейса Renderer
и создание нормальных DTO;
Добавление логирования;
Авторизация;
Работа с prometeus (создание обработчика и написание middleware для сбора статистики по обработчикам).
Свои идеи, предложения и вопросы пишите в комментарии или мне в телеграм.