Simple CRUD in chi. Part 1

Introduction

According to official website,chi is a lightweight, idiomatic, and composable router for creating HTTP services in Go. It is 100% compatible with net/http and is quite easy to use, but its documentation is intended more for experienced developers than for beginners, so I decided to write a series of articles in which we will gradually develop and rework the simplest CRUD written in chi.

In this part, we will write code that will form the basis for further articles. This will be a simple and somewhat dirty code, but this is done intentionally so that the author, together with the reader, can progress from part to part. The code for each subsequent part will appear as you write it in this repository and placed in a separate branch, and all the code written for that part is in this thread.

Preparation

Our CRUD will handle the storage and processing of the following structure:

type CrudItem struct {
    Id          int
    Name        string
    Description string
    internal    string
}

The following two variables will be responsible for storing records:

currentId := 1
storage := make(map[int]CrudItem)

We will save entities in a map/dictionary (what do you prefer?). If it is necessary to add a value to the storage, it is added by key currentId. I want to emphasize that this is a smelly solution and is not intended to be used in real projects. In the next parts we will refactor the storage engine, take it out of the interface and make it thread safe (but not today).

CRUD

The simplest program using chi would look like this:

package main
import (
    "net/http"
    "github.com/go-chi/chi/v5"
)  

func main() {
    r := chi.NewRouter()
    http.ListenAndServe(":3000", r)
}

It does nothing except create the router structure and start its service on port three thousand.
Creating a simple handler and attaching it to the path pattern in chi looks like this:

  1. Select the router method corresponding to the required HTTP method

  2. Pass the path pattern and handler to it http.HandlerFunc (function with signature  func(w http.ResponseWriter, r *http.Request)). Out of the box, the following HTTP methods are available to us:

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)

This is enough to write a standard CRUD, but if you need to write a handler for your own custom HTTP method, you first need to register it using chi.RegisterMethod("JELLO")and then attach a handler to the path pattern in the router using r.Method("JELLO", "/path", myJelloMethodHandler).

Create

Handler registration code for adding a new one CrudItem our improvised storage looks like this:

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
    })

What is our handler implementation:

  1. We are trying to read json from the request body and deserialize it into a structure CrudItem. Valid JSON looks like this:

{
	"name": "New name",
	"description": "New description"
}
  1. If for some reason we were unable to do this, we tell the user that there is something wrong with his request and finish the job.

  2. Assigning entities Id and save it to our storage. Legend has it that in good CRUDs it is customary to return the added object with identifiers assigned to it, and we do the same:

  3. Serializing the structure CrudItem in json;

  4. In case of failure, we tell the user that something went wrong through our fault;

  5. If successful, send json to the user and increment the current one Id.

Read

We will do the reading with two handlers:

  • Read all entries;

  • Read a specific entry. Below is a handler for retrieving all saved records, but for now we are not interested in it – we need it for the following parts:

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)
    })

The handler for receiving a record by 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)
    })

Here we used the receiving id entries from URL. For this we:

  1. Set a named parameter in the path pattern id by using {id};

  2. By using chi.URLParam(r, "id") got the string value of the parameter id;

  3. We tried to provide the parameter id to an integer and, if unsuccessful, informed the user that there was something wrong with their request.

Update

By combining handler implementations to add a new record and retrieve the record by id we can build a handler for updating a record:

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

Removing an entry from our storage looks like this:

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)
    })

Essentially, deleting an entry is deleting a dictionary element with a preliminary check for the presence of the element.

What's next?

This completes the creation of the basic application. Today we implemented CRUD with 5 handlers using the chi router, learned how to read json from the request body, send it in response and get the value from the path pattern.
What the following articles will cover:

  • Refactoring the storage and moving it out of the interface;

  • Pagination for the handler for receiving all records using middleware;

  • Using the Interface Renderer and creating normal DTOs;

  • Adding logging;

  • Authorization;

  • Working with prometeus (creating a handler and writing middleware to collect statistics on handlers).

Write your ideas, suggestions and questions in the comments or to me at telegram.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *