Golang backend with Gin

This post is the learnings that I learnt while creating a golang backend using Gin. A lot of things are different. The mental model while building backend with golang is a little different.

WHAT I WILL BE BUILDING

My main intention was to learn golang by building something. Here, I will be building a backend for creating notes using, Gin, MongoDB and React. I learnt this from youtube where the guy was building the backend only with CRUD operations. I have added some more features, just for my learning and as per my curiosity. Following are the features that you can expect:

  • CRUD operations for notes
  • Listing/Retrieving notes has some variations:
    • Streaming notes:
      • Using SSE
      • Using the “stream()” provided by Gin
    • Infinite notes (used keyset pagination):
      • Only “Next” button for retrieving notes infinitely
        • This can be simulated with infinite scrolling
        • All the notes gets appended with the existing list
      • Both “Next” and “Prev” button for retrieving notes
        • This is a basic paginated list but without page numbers(only previous and next button)
  • Authentication using JWT
  • Authorization with only 2 roles:
    • Admin
    • User

NOTE: I have not added offset pagination as I wanted to learn keyset based and also keyset is more scalable and performance friendly than offset

FOLDER STRUCTURE

I have been writing javascript for years and the folder structure varies a lot. Folder structure was a little different than what I thought. I referred the following link as the reference:

Golang standard folder structure

I have 2 folders as of now:

  • cmd
  • internal

Inside the “cmd” folder I will be naming my app as “api” as I have no intention of making multiple apps. Inside “api”, I will be having “main.go” which will be starting point of our backend app.

main.go

package main
import (
"fmt"
"go_notes_api/internal/config"
"go_notes_api/internal/db"
"go_notes_api/internal/server"
"log"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal("unable to load env variables")
}
client, myDb, err := db.Connect(cfg)
if err != nil {
log.Fatal("unable to connect to DB")
}
defer func() {
if err := db.Disconnect(client); err != nil {
log.Print("error while disconnecting mongo db connection")
}
}()
router := server.NewRouter(myDb, cfg.JwtSecret)
port := fmt.Sprintf(":%s", cfg.ServerPort)
if err := router.Run(port); err != nil {
log.Fatal("server start failed")
}
}

While building golang backend, I realised that golang uses sequential approach heavily. It also uses dependcy injection heavily so that it is predictable(avoiding race conditions) and testable as well.

I have used “go_notes_api” as my module while builiding this app with the “go mod init” command. The current version of golang while building this app was “v1.25.1

As you can see in the “main.go”, it uses results of previous operations in the next operations. Also, it uses error handling out of the box. I was familiar with this approach as I used to write code in this way mostly in Javascript. Always check for errors first. For example, I used to write promises like this in javascript as I have written in the frontend of this app. Below is a code snippet of that:

    async createNote(note) {
        const url = this.generateUrl(this.endpoints["create-notes"])
        const options = {
            headers: {
                "Content-Type": "application/json"
            },
            method: "POST", body: JSON.stringify(note)
        }
        const { res, err } = await fetch(url, options)
        .then(res => {
            if (res.ok) {
                return res.json()
            }
            throw new Error(`HTTP error! status: ${res.status}`);
        })
        .then(data => ({res: data, err: null}))
        .catch(err => ({res: null, err}))

        return {res, err}
    }

I have consumed the above function like this:

      const {err} = await NotesService.createNote(noteData)
      if (err) {
        return
      }

      navigate(APP_ROUTES.NOTES_LIST)

Returning to the “main.go”, we are doing the following things:

  • Loading all the environment variables. If any of the variable is failed to load, the whole app stops before even starting. As golang is a typed language, most issues will get detected while compiling itself
  • Getting the MongoClient and MongoDB instance after establishing the connection to the MongoDB
  • As we know, “defer” gets called just before function ends as a cleanup. So, we are disconnecting the mongo db connection if for some reason app crashes. Here, the function is defined and invoked at the same time
  • Then, we are creating the gin router and starts the app/server using “router.Run()

All the steps are pretty simple and straightforward.

Leave a comment