How to build a CRUD application using Golang ?

CRUD application's picture

Have you ever struggled with building a CRUD application in Golang? You frantically search for a comprehensive guide on building a CRUD app in Go but cannot find one. Well, you are not alone.

Building a CRUD application is one of the first challenges that beginners in programming face and there are very few resources on the internet that provide a comprehensive and detailed guide to building a CRUD app with Go.

This tutorial will take you through building a basic CRUD application using Golang from scratch. In case you do not know already, CRUD is an acronym for Create, Read, Update and Delete operations that almost every application performs on a database.

The application we are going to build is a Todo application which will have functionality for listing todos, creating todos, updating todos and deleting todos. Here is how the application will look like.

Development environment setup and prerequisites

I have used Visual Studio Code for this tutorial, however, you can use any code editor and the code will work just fine. It is essential to have Go and MongoDB server installed before beginning development. Go can be downloaded at https://go.dev/. MongoDB Community Server can be downloaded at https://www.mongodb.com/try/download/community.

Once Go is installed, a folder named ‘go’ is created which usually contains a sub folder ‘src’. In case the sub folder ‘src’ did not get created you can always create one. We will now create our project inside ‘go/src’. Depending on your operating system you will open either Terminal or Powershell in order to execute the subsequent commands. In case you are a Mac user you will visit Terminal while Windows users can use Powershell. The commands will be slightly different for Mac users.

To begin with, change your directory to ‘go/src’ by running the following command.

cd go/src

Create a new project by running the following command.

mkdir golang-todo

Go inside the directory by running the following command.

cd golang-todo

Let us now create a go.mod file by running the ‘go mod init’ command. Each Go module is defined by a ‘go.mod’ file that describes the module’s properties, including its dependencies on other modules and on versions of Go. The ‘go.mod’ file is similar to ‘package.json’ for Node.js applications.

The complete command is displayed below.

go mod init golang.company/golang-todo

We will start installing packages now. The first package that we will install is chi. chi is a router for building Go HTTP services. Run the following command to install chi.

go get “github.com/go-chi/chi”

The next package that we will be installing is renderer. Renderer is a response rendering package for Go. We will run the following command to install renderer.

go get “github.com/thedevsaddam/renderer”.

We will install the MongoDB driver for Go next by running the following command.

go get “gopkg.in/mgo.v2”

Let us open Visual Studio Code now by running the following command.

code .

Create main.go file

When you develop executable programs, you will use the package “main” for making the package as an executable program. The package “main” tells the Go compiler that the package shou

ld compile as an executable program instead of a shared library. The main function in the package “main” will be the entry point of our executable program.

To begin with, we will start importing required packages.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( "context" "encoding/json" "log" "net/http" "os" "os/signal" "strings" "time" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/thedevsaddam/renderer" mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" )

To begin with, we declare a main package (a package is a way to group functions, and it's made up of all the files in the same directory).

‘encoding/json’ is a package that helps in reading and writing JSON data from your Go programs.

‘log’ package is used for logging errors.

‘net/http’ is a package that provides HTTP client and server implementations.

‘strings’ is a package that implements simple functions to manipulate UTF-8 encoded strings.

‘time’ is a package that provides functionality for measuring and displaying time.

‘context’, ‘os’ and ‘os/signal’ are used to create and stop channels.

‘github.com/go-chi/chi’, ‘github.com/go-chi/chi/middleware’, ‘github.com/thedevsaddam/renderer’, ‘gopkg.in/mgo.v2’ and ‘gopkg.in/mgo.v2/bson’ are the third party packages discussed previously. In particular, ‘gopkg.in/mgo.v2/bson’ is an implementation of the BSON specification for Go.It is used for handling the BSON data format that is natively supported in MongoDB.

We will then create a variable that will help us use renderer in our application.

var rnd *renderer.Render

We then create a variable that will help our application interact with our database.

var db *mgo.Database

We create a few constants now so that they can be reused throughout the application.

1 2 3 4 5 6 const ( hostName string = "localhost:27017" dbName string = "demo_todo" collectionName string = "todo" port string = ":9000" )

‘hostName’ refers to the MongoDB host.
‘dbName’ refers to the name of the database.

‘todo’ is the name of the collection here. Collections are analogous to tables in relational databases.

‘port’ here refers to the port at which the application will run.

We will now define two structs both representing our database model, one handling BSON data for our database and the other handling JSON data for our front-end.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type ( todoModel struct { ID bson.ObjectId `bson:"_id,omitempty"` Title string `bson:"title"` Completed bool `bson:"completed"` CreatedAt time.Time `bson:"createAt"` } todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` } )

A structure or struct in Golang is a user-defined type that allows to group/combine items of possibly different types into a single type. Any real-world entity which has some set of properties/fields can be represented as a struct. This concept is generally compared with the classes in object-oriented programming. It can be termed as a lightweight class that does not support inheritance but supports composition.

We will now create an init function that will establish a connection with the database, start a session and initialise a renderer.

1 2 3 4 5 6 7 func init() { rnd = renderer.New() sess, err := mgo.Dial(hostName) checkErr(err) sess.SetMode(mgo.Monotonic, true) db = sess.DB(dbName) }

Usage of the MongoDB driver revolves around the concept of sessions. The Dial function obtains a session and establishes one or more connections with the cluster of servers defined by the url parameter.SetMode changes the consistency mode for the session. In the Strong consistency mode reads and writes will always be made to the primary server using a unique connection so that reads and writes are fully consistent, ordered, and observing the most up-to-date data. This offers the least benefits in terms of distributing load, but the most guarantees. In the Monotonic consistency mode reads may not be entirely up-to-date, but they will always see the history of changes moving forward, the data read will be consistent across sequential queries in the same session, and modifications made within the session will be observed in following queries (read-your-writes). The Eventual mode is the fastest and most resource-friendly, but is also the one offering the least guarantees about ordering of the data read and written. Monotonic is used most often.

We then define the checkErr function which checks for errors and logs them.

1 2 3 4 5 6 7 8 9 func checkErr(err error) { if err != nil { log.Fatal(err) } }

Create main function

We will implement the main now. A main function executes by default when you run the main package.

1 2 3 4 5 func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", homeHandler) r.Mount("/todo", todoHandlers())

Apart from creating a router and logger we are defining routing logic here. For “/” route ‘homeHandler’ function will be invoked and for “/todo” ‘todoHandlers’ function will be invoked.

Let us define the ‘todoHandlers’ function now.

1 2 3 4 5 6 7 8 9 10 func todoHandlers() http.Handler { rg := chi.NewRouter() rg.Group(func(r chi.Router) { r.Get("/", fetchTodos) r.Post("/", createTodo) r.Put("/{id}", updateTodo) r.Delete("/{id}", deleteTodo) }) return rg }

We are using the ‘http’ package here. We are creating a group router here for all routes beginning with "/todo". These routes collectively provide CRUD functionality with the functions ‘fetchToDos’, ‘createToDo’, ‘updateToDo’ and ‘deleteToDo’.

We create a server next by defining the port, Handler, ReadTimeout, WriteTimeout and IdleTimeout properties.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 srv := &http.Server{ Addr: port, Handler: r, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Println("Listening on port ", port) if err := srv.ListenAndServe(); err != nil { log.Printf("listen: %s\n", err) } }()



ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections.

We will now write the code inside the main function to stop the server gracefully.

1 2 3 stopChan := make(chan os.Signal) signal.Notify(stopChan, os.Interrupt)

We call the stopChan function at the end of the main function.

1 2 3 4 5 6 7 8 9 10 11 <-stopChan log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) srv.Shutdown(ctx) defer cancel() log.Println("Server gracefully stopped!")

Here is how the complete main function looks like now.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 func main() { stopChan := make(chan os.Signal) signal.Notify(stopChan, os.Interrupt) r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", homeHandler) r.Mount("/todo", todoHandlers()) srv := &http.Server{ Addr: port, Handler: r, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Println("Listening on port ", port) if err := srv.ListenAndServe(); err != nil { log.Printf("listen: %s\n", err) } }() <-stopChan log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) srv.Shutdown(ctx) defer cancel() log.Println("Server gracefully stopped!") }

We will now define the ‘homeHandler’ function.

1 2 3 4 5 6 7 func homeHandler(w http.ResponseWriter, r *http.Request) { err := rnd.Template(w, http.StatusOK, []string{"static/home.tpl"}, nil) checkErr(err) }

All that the ‘homeHandler’ does is render a template on navigation to the route “/”. The template file is home.tpl located inside the ‘static’ folder.

Fetch todos

We now create the ‘fetchTodos’ function that fetches a list of todos from the database.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func fetchTodos(w http.ResponseWriter, r *http.Request) { todos := []todoModel{} if err := db.C(collectionName). Find(bson.M{}). All(&todos); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to fetch todo", "error": err, }) return } todoList := []todo{} for _, t := range todos { todoList = append(todoList, todo{ ID: t.ID.Hex(), Title: t.Title, Completed: t.Completed, CreatedAt: t.CreatedAt, }) } rnd.JSON(w, http.StatusOK, renderer.M{ "data": todoList, }) }

We define two variables todos and todoList where todos is the slice of the struct ‘todoModel’ that returns BSON data from the MongoDB database and todoList is the slice of the struct todoList that returns JSON data to the front-end.

1 2 3 4 5 todos := []todoModel{} todoList := []todo{}

We then query from the collection for todos which are returned in BSON format.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if err := db.C(collectionName). Find(bson.M{}). All(&todos); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to fetch todo", "error": err, }) return }

We then loop through the todos and return a JSON array of todos that is sent to the front-end for rendering the todo list.

1 2 3 4 5 rnd.JSON(w, http.StatusOK, renderer.M{ "data": todoList, })

Create todos

We will write the code for ‘create todo’ functionality now.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func createTodo(w http.ResponseWriter, r *http.Request) { var t todo if err := json.NewDecoder(r.Body).Decode(&t); err != nil { rnd.JSON(w, http.StatusProcessing, err) return } // simple validation if t.Title == "" { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The title field is requried", }) return } // if input is okay, create a todo tm := todoModel { ID: bson.NewObjectId(), Title: t.Title, Completed: false, CreatedAt: time.Now(), } if err := db.C(collectionName).Insert(&tm); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to save todo", "error": err, }) return } rnd.JSON(w, http.StatusCreated, renderer.M{ "message": "Todo created successfully", "todo_id": tm.ID.Hex(), }) }

We use the type ‘Decoder’ of the ‘json’ package to decode the request body and store it in a variable ‘t’ which is of type ‘todo’ a JSON object. If any errors are encountered we return them.

1 2 3 4 5 6 var t todo if err := json.NewDecoder(r.Body).Decode(&t); err != nil { rnd.JSON(w, http.StatusProcessing, err) return }

We then check if the title is not empty. In case it is empty we return an error.

1 2 3 4 5 6 if t.Title == "" { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The title field is requried", }) return }

We then create a variable tm of type ‘todoModel’ which is BSON data.

1 2 3 4 5 6 7 8 9 10 11 tm := todoModel { ID: bson.NewObjectId(), Title: t.Title, Completed: false, CreatedAt: time.Now(), }

The next step is to insert this BSON object into the collection and return any error if encountered.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 if err := db.C(collectionName).Insert(&tm); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to save todo", "error": err, }) return }

The last step is to return a message to the user on successful creation of a todo.

1 2 3 4 5 6 7 rnd.JSON(w, http.StatusCreated, renderer.M{ "message": "Todo created successfully", "todo_id": tm.ID.Hex(), })

Delete todos

We will now write the code to implement ‘delete todo’ functionality.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func deleteTodo(w http.ResponseWriter, r *http.Request) { id := strings.TrimSpace(chi.URLParam(r, "id")) if !bson.IsObjectIdHex(id) { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The id is invalid", }) return } if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to delete todo", "error": err, }) return } rnd.JSON(w, http.StatusOK, renderer.M{ "message": "Todo deleted successfully", }) }


To begin with, we parse the URL parameter ‘id’ using the ‘URLParam’ method of the ‘chi’ package and store it in a variable ‘id’.

1 id := strings.TrimSpace(chi.URLParam(r, "id"))

We then check if ‘id’ is a valid hex representation of an ObjectId using the IsObjectIdHex method and return an error in case it is not.

1 2 3 4 5 6 if !bson.IsObjectIdHex(id) { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The id is invalid", }) return }

We then use the ‘RemoveId’ method of the MongoDB API to delete a ‘todo’ record from the collection.

1 2 3 4 5 6 7 if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to delete todo", "error": err, }) return }

Lastly, we return a message to the user on successful deletion of the ‘todo’ record from the collection.

1 2 3 4 5 rnd.JSON(w, http.StatusOK, renderer.M{ "message": "Todo deleted successfully", })


Update todo

We will now develop the only functionality remaining which is ‘update todo’.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func updateTodo(w http.ResponseWriter, r *http.Request) { id := strings.TrimSpace(chi.URLParam(r, "id")) if !bson.IsObjectIdHex(id) { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The id is invalid", }) return } var t todo if err := json.NewDecoder(r.Body).Decode(&t); err != nil { rnd.JSON(w, http.StatusProcessing, err) return } if t.Title == "" { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The title field is required", }) return } if err := db.C(collectionName). Update( bson.M{"_id": bson.ObjectIdHex(id)}, bson.M{"title": t.Title, "completed": t.Completed}, ); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to update todo", "error": err, }) return } rnd.JSON(w, http.StatusOK, renderer.M{ "message": "Todo updated successfully", }) }

To begin with, we fetch the id by parsing the URL parameter using the ‘chi’ package.

1 id := strings.TrimSpace(chi.URLParam(r, "id"))

We then check if ‘id’ is a valid hex representation of an ObjectId using the ObjectIdHex function provided by the ‘bson’ package.

1 2 3 4 5 6 7 8 9 10 11 if !bson.IsObjectIdHex(id) { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The id is invalid", }) return }

We then decode the request body using the ‘json’ package and store it in a variable ‘t’. Errors, if any, are returned.

1 2 3 4 if err := json.NewDecoder(r.Body).Decode(&t); err != nil { rnd.JSON(w, http.StatusProcessing, err) return }

We then check if the title is empty, in case it is not, an error is returned.

1 2 3 4 5 6 if t.Title == "" { rnd.JSON(w, http.StatusBadRequest, renderer.M{ "message": "The title field is required", }) return }

We then write the code for updating the todo into the collection using the ‘Update’ method of the MongoDB API.

1 2 3 4 5 6 7 8 9 10 11 if err := db.C(collectionName). Update( bson.M{"_id": bson.ObjectIdHex(id)}, bson.M{"title": t.Title, "completed": t.Completed}, ); err != nil { rnd.JSON(w, http.StatusProcessing, renderer.M{ "message": "Failed to update todo", "error": err, }) return }

If the update succeeds, a ‘success’ message is returned to the user as displayed below.

1 2 3 rnd.JSON(w, http.StatusOK, renderer.M{ "message": "Todo updated successfully", })

Building and running the application

We build the Go application using the following command.

go build

In case there are no build errors you can run the application using the following command.

go run main.go

Once the application runs successfully you will see the following in your Powershell.

2022/07/04 11:12:41 Listening on port :9000

On entering the URL ‘localhost:9000’ in the browser you will be able to see your application running in the browser as displayed below.

Building and running the application

You can test the application by performing each of the CRUD operations. Each CRUD operation that you perform is logged in the Powershell as displayed below.

2022/07/04 11:16:53 "GET http://localhost:9000/ HTTP/1.1" from [::1]:57997 - 200 7571B in 2.4222582s

2022/07/04 11:16:55 "GET http://localhost:9000/todo HTTP/1.1" from [::1]:57997 - 200 11B in 2.981ms

2022/07/04 11:16:56 "GET http://localhost:9000/favicon.ico HTTP/1.1" from [::1]:57997 - 404 19B in 0s

2022/07/04 11:19:56 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58104 - 201 76B in 362.3593ms

2022/07/04 11:20:53 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58114 - 201 76B in 116.5622ms

2022/07/04 11:21:44 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58118 - 201 76B in 2.2108ms

2022/07/04 11:22:08 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58122 - 201 76B in 6.9052ms

2022/07/04 11:22:53 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58124 - 201 76B in 6.2156ms

2022/07/04 11:23:12 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58129 - 201 76B in 7.5956ms

2022/07/04 11:23:55 "POST http://localhost:9000/todo HTTP/1.1" from [::1]:58132 - 201 76B in 3.5623ms

2022/07/04 11:24:24 "PUT http://localhost:9000/todo/62c27fbdd0c48545001072ae HTTP/1.1" from [::1]:58136 - 200 39B in 120.0582ms

2022/07/04 11:24:34 "DELETE http://localhost:9000/todo/62c27f83d0c48545001072ad HTTP/1.1" from [::1]:58137 - 200 39B in 40.3621ms

Conclusion

In this article we discussed how to build a basic CRUD application with Go using MongoDB as a database.

Happy programming!

Build Your Golang Team