Простой CRUD на chi. Часть 1

от автора

Введение

Согласно официальному сайту, 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. Я хочу подчеркнуть, что это решение с запахом и не предназначено для использования в реальных проектах. В следующих частях мы отрефакторим механизм хранения, вынесем его за интерфейс и сделаем его потокобезопасным (но не сегодня).

CRUD

Простейшая программа с использованием chi будет выглядеть так:

package main import (     "net/http"     "github.com/go-chi/chi/v5" )    func main() {     r := chi.NewRouter()     http.ListenAndServe(":3000", r) } 

Она ничего не делает, кроме создания структуры маршрутизатора и запуска его обслуживания на трехтысячном порту.
Создание простейшего обработчика и навешивание его на паттерн пути в chi выглядит следующим образом:

  1. Выбрать метод марштрутизатора, соответствующий необходимому HTTP-методу

  2. Передать в него паттерн пути и обработчик 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).

Create

Код регистрации обработчика для добавления нового 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     }) 

Что из себя представляет наша реализация обработчика:

  1. Пытаемся прочитать из тела запроса json и десериализовать его в структуру CrudItem. Валидный JSON выглядит так:

{ "name": "New name", "description": "New description" } 
  1. Если по какой-то причине нам не удалось это сделать, мы говорим пользователю о том, что с его запросом что-то не так и заканчиваем работу.

  2. Присваиваем сущности Id и сохраняем в наше хранилище. Ходят легенды, что в хороших CRUD-ах принято возвращать добавленный объект с присвоенными ему идентификаторами, и мы поступаем так же:

  3. Сериализуем структуру CrudItem в json;

  4. В случае провала говорим пользователю, что что-то пошло не так по нашей вине;

  5. В случае успеха отправляем пользователю json и инкрементим текущий Id.

Read

Чтение мы сделаем двумя обработчиками:

  • Прочитать все записи;

  • Прочитать конкретную запись. Ниже приведен обработчик для получения всех сохраненных записей, но пока он нам не интересен — он нужен нам для следующих частей:

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. Для этого мы:

  1. Задали в паттерне пути именной параметр id с помощью {id};

  2. С помощью chi.URLParam(r, "id") получили строковое значение параметра id;

  3. Попробовали привести параметр id к целому числу и в случае провала сообщили пользователю, что с его запросом что-то не так.

Update

Объединив реализации обработчика для добавления новой записи и получения записи по 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)     }) 

Delete

Удаление записи из нашего хранилища выглядит следующим образом:

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 для сбора статистики по обработчикам).

Свои идеи, предложения и вопросы пишите в комментарии или мне в телеграм.


ссылка на оригинал статьи https://habr.com/ru/articles/800787/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *