Building a CRUD API with Golang, Gin, Gorm, Bit.io

by, KCanamar, 2023-03-08

So you want to build an api with GO? Let's do it. The following is a step by step guide on how to build a full CRUD api using Gin. Then we will go one step futher and deploy our api to Render.com.

Prerequisites

There are not that many just the normal housekeeping so we can code together.

  • Have VsCode or a text editor - Download VsCode
  • Need Golang installed on your machine - Download Golang
  • Register for a FREE account with Bit.io
  • Register for a FREE account with Render.com
  • Have Postman or equivalent installed on your machine Postman, Dont use the browser based Api Tests they will not work with localhost development
  • Need a Github Account to host your code. Github

I will be linking along the way the links to relevant documentation in case you run into any errors or need futher clarification. Look for these Check The Docs along the way.

We're gonna learn Today!

Goals

  1. Be able to write a RESTful API using Golang, Gin.
  2. Connect to bit.io to persist our data.
  3. Deploy our api to render.com
Take Note - when working with Go is that there is a specific way we have to structure our directories and subsequet files. I am not going to be covering how or why in this post but feel free to Check The Docs

Open terminal and navigate to where go code is stored - you can check to see where after installing Go by running command go env

cd <where ever you have go installed>

Next run the bash command

mkdir github.com

Now we are going to move our terminal into that newly created directory

   cd github.com

Bring it back now y'all, okay enough fun. How about we create another directory this time named after our github profile, run the bash command

mkdir username

Now lets navigate into the newly created directory with the command.

   cd username

Alright now let's do that process on more time but for what we want our project to be called, for our purposes let us call this go-gin-bit-crud

mkdir go-gin-bit-crud

Now, you guessed it we are going to change directories again to our newly created project directory.

cd go-gin-bit-crud

Now that we are in the correct directory lets go ahead and open up this directory in vscode, in our terminal enter the following command. *If you are unable to open vscode this way Check the Docs *

code .

Note - Be sure to have the Go extension for vscode Check the Docs

Start here if you are familiar with setting up your go projects, and having it open in VsCode

Now since we are going to be deploying this on render.com we are going to need to make sure that we initialize a git repo in our project directory. We do this by running the folowing command.

git init

Now we should make our first commit to start our git history. rRuning the following commands

git add .
git commit -m "first commit"

Now we need to create the go equivalent of a package.json , might seem similar if you are coming from node.js . The go.mod acts as our package manager, this is where we will be installing all of our dependencies for this project. So let us opne up a terminal in vscode in our project directory and run the following command.

go mod init

Here come the Packages

We are goin to be using CompileDaemon to watch .go files and invokes go build if a file changed, Check the Docs , again another node.js reference this will be similar to nodemon

go get github.com/githubnemo/CompileDaemon

Now we want to be able to use CompileDaemon from the command line, so we are going to have to install it.

go install github.com/githubnemo/CompileDaemon

Alright now we are going to want to setup our environment variables, for this we are going to need the package godotenv - Check the Docs

go get github.com/joho/godotenv

Let's keep it moving by adding our web framework package GIN - Check the Docs

go get -u github.com/gin-gonic/gin

Now that we have our web framework we are also going to want a way to talk with our database so for this project we are going to be using the ORM GORM - Check the Docs

go get -u gorm.io/gorm

We are going to need add a second part of GORM this will be the postgreSQL driver that way we can communicated with bit.io - Check the Docs

go get -u gorm.io/driver/postgres

Finally, we can start writing some code

Lets keep it going by staying in the terminal for a little longer, create a main.go file in the projects root directory.

touch main.go

Here is the code that will be in main.go to make sure that we have all of our packages in our go.mod setup correctly.

// main.go

// delcare package main
package main

import (    
    "fmt"
)

// declare main function
func main() {
    fmt.Println("Hola")
}

Test to make sure that compiledaemon is working with command.

CompileDaemon -command="./project_name"

Let us verify that the canges are happening by changing the return value to fmt.Println("Hi {your name}")

Since we have verified that CompileDaemon is running and our server is refreshing with changes let us setup our basic server listening on our home route. Check the Docs

// main.go

import "github.com/gin-gonic/gin"

func main() {
    // setup a gin router
    router := gin.Default()
    // Declare home get route
    router.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
        "message": "Hello from the other side!",
        })
    })
    // setup our server listener and serve on PORT 0.0.0.0:8080
    router.Run()
}

In our browser, I am using Chrome, lets navigate to localhost:3000 and confirm we see our message.

Since we are going to deploying this we are going to want to setup our dotenv package by touching a .env file into the root directory along with a .gitignore that way our git commit doesn't expose our environment variables, if you have a global .gitignore you are ahead of the game.

touch .env .gitignore

Define a PORT variable in the .env file and set to PORT=3000 we are also going to be adding in an ENV variable to help with deployment, this will come in handy later.

// .env

PORT=3000
ENV="development"

Lets make sure to setup our .gitignore too.

// .gitignore

.env

How about we connect the .env to our main.go . We are going to have to create a func init() that will load our environment variables.

import (
    "log"
    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
)

func init() {
    err := godotenv.Load()
    
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}

func main(){ ...

Now to keep our code looking polished lets move this env section in our init() to its own file, we can start this by adding a new directory into root directory of the project.

mkdir initializers

Then lets add a file to hold our code.

touch initializers/loadEnv.go

Now we can relocate the init functions contents to initializers/loadEnv.go and beef it up a little more, we are going to check our file path for our environment variables since deploying on render.com has its own challenges, and this will make sure that our deployment goes smooth. Check the Docs

//loadEnv.go

package initializers

import (
    "log"
    "os"
    "path/filepath"
    "github.com/joho/godotenv"
)

// Besure to use PascalCase when naming your Helper functions
func LoadEnv() {

    // assign dir to the root directory of our preject
    dir, err := filepath.Abs(filepath.Dir(os.Args[0]))

    // check for errors
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    // assign our environment path to the .env file of the root directory
    environmentPath := filepath.Join(dir, ".env")

    // reassign error to the return value of godotenv
    err = godotenv.Load(environmentPath)

    // Check for errors
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}

After we have setup provide our environment variables to our server, for this we will need to import the initializers package into main.go.

//main.go
...
import (
    "github.com/gin-gonic/gin"
    "github.com/kcanamar/go-gin-bit-crud/initializers"
)

func init() {
    // this will safegaurd our production deployment
    if os.Getenv("ENV") != "production" {
        initializers.LoadEnv()
    }
}
...

Connect to Bit.io

Now lets connect a database, touch another file into the initializers directory

touch initializers/database.go

For this connection we will be using bit.io as our database, bit.io software is a platform used to secure Postgres database. If you do not have a bit.io account head over to bit.io and create a free account here

Now you that you have created/ logged in to your bit.io account, you will see a button by the top left of the dashboard labeled + new. For the purposes of this walkthough you can name this database however you like.

Once the database is setup we are going to select the connect opiton. You will find this near the top right of the screen, once selected our dashboard will now display our connection information. Be sure to leave this tab open.

Now that we have our database setup and on the connection screen lets setup database.go, first we should delcare what package this file belongs to, and import our Gorm dependencies.

//database.go

package initializers

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func ConnectDB() {

    dsn := "{your postgresql connection string}?sslmode=allow"

}

IMPORTANT - we need to add the query parameter sslmode and set the value to allow, this is due to the security setting associated with bit.io Check the Docs

Next we will create a global variable to access in any of the files var DB *gorm.DB Then we need to impement some error handling local scoped to our ConnectDB() function var err error Next we are going to asign the variables we have created to the result of our connection attempt, handling our errors if any with a fatal response log.Fatal("Failed to connect to the database") Check the Docs

//database.go
package initializers

import (
    "log"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)
// create a global variable for Database
var DB *gorm.DB

func ConnectDB() {
    // create local scoped error
    var err error
    
    dsn := "{your postgresql connection string}?sslmode=allow"
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    
    // error handling
    if err != nil {
        log.Fatal("Failed to connect to database")
    }
}

Now that we have all of this running when we check our termial running compiledaemon we should see no errors. However if you do encounter one please go back and check for syntaxing errors, or consult the supporting documentation provided at each step.

No error, let's go! Since we are currently exposing our database connection string, a huge security risk, let's relocate that string into our .env file with a key name of DATABASE_URL

// .env

PORT=3000
ENV="development"
DATABASE_URL={your postgresql connection string}

Now we use that variable is safely hidden in our environment variables, lets up date our code to use our connection string with os.Getenv("Variable_Name")

//database.go
package initializers

import (
    "log"
    "os"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)
var DB *gorm.DB

func ConnectDB() {
    var err error

    // .env variable
    dsn := os.Getenv("DATABASE_URL")
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {
        log.Fatal("Failed to connect to database")
    }
}

With the database connected, lets move on and create some models for our data, for this we will need to add a models directory in the project's root directory.

mkdir models

Next

touch models/postModel.go

We are going to be embedding the gorm.model to include our ID, CreatedAt, UpdatedAt, DeletedAt fields to our struct with our haing to write them all out, thank you gorm. Check the Docs

We are going to add two more fields to our struct, since this is a Post it should have at minimum a Title and Body both as string datatypes.

// postModel.go
package models

import "gorm.io/gorm"

type Post struct {
    gorm.model
    Title string
    Body string
}

Now that we have a model setup, we need to migrate to our database. Todo this we are going to create, you guessed it, another directory in the root directory mkdir migrate, then touch migrate/migrate.go

mkdir migrate

Then

touch migrate/migrate.go

Now that we have our file created lets add some dependencies. First we are going to need our initializers package, this will enable us to access both LoadEnv() and ConnectDB() funcs from with in migrate.go's init func. Second we are going to need our models package, pretty straight forward, this will describe the shape our data(schema) must be in to reach the database. Finally in our main func we are going to utilize our global DB variable to AutoMigrate our data. Check the Docs

// migrate.go
package main

import (
    // Import dependencies
    "github.com/kcanamar/go-gin-bit-crud/initializers"
    "github.com/kcanamar/go-gin-bit-crud/models"
)

func init() {
    // Import initializers
    initializers.LoadEnv()
    initializers.ConnectDB()
}

func main() {
    // AutoMigrate takes a model struct argument
    initializers.DB.AutoMigrate(&models.Post{})
}

Once the migrte file is all setup lets migrate with command

go run migrate/migrate.go

Create

So long as there are no errors we will see the table created in the data section of bit.io. Take a moment to celebrate, we have just now completed setting up our API with Bit.io. Now off to do some CRUD operations with our Post model.

I know what you are going to say, "KCan, are we really going to make another directory?", short answer - Yup. With our terminal in the project root directory

mkdir controllers

and then

touch controllers/postCtrl.go

In postCtrl.go we will be defining the route controls for GET, POST, PUT, and DELETE requests related to our post model. We will start off by gathering our dependencies, from Gin. Then start our process with the "C" in CRUD, Create. Let's take a small step first by, sending back a successful message when the PostsCreate controller is called.

// postCtrl.go
package controllers

import "github.com/gin-gonic/gin"

func PostsCreate(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "Hello from the other side!",
    })
}

Next, we need to connect the server to our database, then mount the controllers within our server. We are going to need to import our controllers package. Since we already have the initializers package imported we already have access to ConnectDB(). Now in our home route, lets add in our newly created controller and check to make sure that everything is function as intended.

// main.go
package main

import (
    "os"
    "github.com/gin-gonic/gin"

    // import controller package
    "github.com/kcanamar/go-gin-bit-crud/controllers"
    "github.com/kcanamar/go-gin-bit-crud/initializers"
)

func init() {
    if os.Getenv("ENV") != "production" {
        initializers.LoadEnv()
    }
    // connect the database
    initializers.ConnectDB()
}

func main() {
    router := gin.Default()
    
    // include the contoller
    router.GET("/", controllers.PostsCreate)
    
    router.Run()
}

Lets head over to localhost:3000 to make sure the server is giving use the feedback we want. Now lets modify our code to send some data using a POST request

// main.go
...

func main() {
    router := gin.Default()
    
    // Create Route should use a POST http verb
    router.POST("/posts", controllers.PostsCreate)
    
    router.Run()
}

Now that we are using the right Http grammer, lets head to postCtrl.go to refactor our code so we can send data to our database with a payload.

First let's go ahead and create a new struct reqBody based on our model to hold the request information. Once we have our reqBody struct we need to bind our JSON data with it, we achieve this by calling the Bind method from *gin.context, passing a pointer to our reqBody struct. Check the Docs

Second we are going to use our reqBody to populate a newPost struct based on our Post modelCheck the Docs. Now that we have our newPost built from our reqBody we need to send this data to our database, creating the new post. This will be achieved by creating a variable result and assinging it the result of Gorm's .Create method with the argument of a pointer to newPost.

Third, we need to implement some error handling incase something unexpected happens.

Lastly, we will respond with a 200 status and provide postive feedback with the newly created posts data.

// postCtrl.go
package controllers

import (
    "github.com/gin-gonic/gin"
    "github.com/kcanamar/go-gin-bit-crud/initializers"
    "github.com/kcanamar/go-gin-bit-crud/models"
)

func PostsCreate(c *gin.Context) {

    // Get data from request body
    // create variable
    var reqBody struct {
        Title string
        Body string
    }

    // parse the request body as JSON then binds to reqBody via a pointer
    c.Bind(&reqBody)

    // Create a newPost 
    newPost := models.Post{Title: reqBody.Title, Body: reqBody.Body}
    result := initializers.DB.Create(&newPost)
  
    // handle error from result
    if result.Error != nil {
        c.Status(400)
        return
    }
    // return post
    c.JSON(200, gin.H{
        // send back a confirmation of the post
        "newPost": newPost,
    })
}

Time to test, open up postman so we can send a JSON body to our local host server. In post man we are going to create a new POST request to localhost:3000/posts sending a body as raw data in a JSON format. Once we have the proper settings selected, and populated our request body payload, send the request. Note - If you are struggling to send your request Check the Docs

We see that it is created! Happy dance!

Read

Let's keep that momentum going, onto the "R" in CRUD, this is going to be split up into two different routes. One to find all of the posts, and another to find just a single post.

Let's kick it off by making a new function in our postCtrl.go file called PostsIndex and here we will be getting all of the posts from our database and sending them back in the response as JSON.

Similar to our PostsCreate func we need to create an array variable allPosts typed of our Post model, this will hold all of our posts. Once we have our variable we are going to use the Gorm method .Find to query our database, passing the argument of a pointer to allPosts. Finally we will respond with all of our posts. Check the Docs

// postCtrl.go

...

func PostsIndex(c *gin.Context){
    
    // variable to hold posts
    var allPosts []models.Post

    // Query the database
    initializers.DB.Find(&allPosts)
    
    // Response with posts
    c.JSON(200, gin.H{
        "posts": allPosts,
    })
}

Let's now update our router with this new route, back to main.go.

// main.go

func main() {
    router := gin.Default()

    // Index Route
    router.GET("/posts", controllers.PostsIndex)

    router.POST("/posts", controllers.PostsCreate)
    
    router.Run()
}

Check to make sure this works by sending a GET request, in postman to localhost:3000/posts. We should see a JSON object with a key of "posts" with a value of an array of posts.

Air Guitar Most excellent!

Now that we can get all of the posts, let focus in to go and get a sinlge post for our Show route, head back to postCtrl.go and lets create another func PostShow.

Now that we have seen the pattern a few times, lets recreate it one more time. Create a variable post type of models.Post. Next we will use the Gorm method .First passing in two arguments. the first argument with be a pointer to the foundPost variable, and the second argument will be the id of the post provided by the request parameters. Finally responding with the foundPost. Check the Docs

// postCtrl.go

...

func PostShow(c *gin.Context){
    
    // variable to hold post
    var foundPost models.Post

    // Query the database
    initializers.DB.First(&foundPost, c.Param("id"))

    // Response
    c.JSON(200, gin.H{
        "post": foundPost
    })
}

How about we add the show controller to our router. We're going, going, back, back to main.go.

// main.go

... 

func main() {

    router := gin.Default()

    // Added in the home route just to have a route route for testing the API
    router.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "Hello from the other side!",
        })
    })

    router.GET("/posts", controllers.PostsIndex)
    router.POST("/posts", controllers.PostCreate)

    // Show Route
    router.GET("/posts/:id", controllers.PostShow)

    router.Run()

}

Time to test that out by sending a GET request to localhost:3000/posts/1 and we should expect to see the post with id=1.

Update

Go ahead and Celebrate 2/4 of the way through CRUD.

Almost there lets update a post now, back to postCtrl.go to create our update controller. In this example lets showcase that we can obtain the request parameter another way, for this we will create a variable id assigned with the request param id. Next we need to create a variable for our reqBody again referencing the Post models properties. We are also going to need a variable to represnt our Post model.

First we query the database to find the post to updated we will be using Gorm's .Find method with two arguements, first argument will be a pointer to the Post model variable, second argument will be the id variable.

Then we are going to chain some Gorm methods together starting with .Model with the argument of a pointer to updatedPost. Next method in this chain will be .Update passing the argument of a struct typed of our model Post populated with our data from the reqBody variable.Check the Docs

Finishing this off by responding with the updatedPost.

// postsCtrl.go

...

func PostUpdate(c *gin.Context){
    // Get the id from params
    id := c.Param("id")
    
    // Get the request body
    var reqBody struct {
        Title string
        Body string
    }
    
    // Find post to be updated
    var updatedPost models.Post
    initializers.DB.Find(&updatedPost, id)
    
    // Update Post
    initializers.DB.Model(&updatedPost).Updates(models.Post{
        Title: reqBody.Title,
        Body: reqBody.Body,
    })
    
    // Respond with updated post
    c.JSON(200, gin.H{
        "post": updatedPost
    })
}

Now, you guessed it, update our router in main.go with the new controller

// main.go

...
    router.GET("/posts", controllers.PostsIndex)
    router.POST("/posts", controllers.PostCreate)

    // Update Route
    router.PUT("/posts/:id", controllers.PostUpdate)

    router.GET("/posts/:id", controllers.PostShow)
...

Time to test, using postman send a JSON body as a PUT request to localhost:3000/posts/1.

// Request body
{
    "title": "updated"
}

Note- You can update any number of properties defined in the request body, or just one.

Air Horn Let's GO, we should see our post updated with the new title. We are right there 3/4 of CRUD complete, 4th Quarter, 90th Minute, let finish strong.

Delete

Now lets DELETE a post to round out our C.R.U.D. Operations, back to postCtrl.go. This is a fairly straight forward controller, we are going to access the Grom method .Delete passing two arguments. First argument will be a pointer to our Post model, with the second argument being the request param id of the post to be deleted. All that is left is responding with a success method. Check the Docs

// postCtrl.go

...

func PostDestroy(c *gin.Context) {
    // Delete the post
    initializers.DB.Delete(&models.Post{}, c.Param("id"))

    // Response
    c.JSON(200, gin.H{
        "message": "Successful Destory",
    })

}

Another one bites the dust, time to add out controller to our router in main.go.

// main.go
...
    router.POST("/posts", controllers.PostCreate)
    router.PUT("/posts/:id", controllers.PostUpdate)
    
    // Delete Route
    router.DELETE("/posts/:id", controllers.PostDestroy)

    router.GET("/posts/:id", controllers.PostShow)
...

Time to test, using postman send a DELETE request to localhost:3000/posts/1.

CRUD Complete

Take a moment and celebrate, we have just built a full CRUD API using Golang, Gin, Gorm, and persiting our data using Bit.io. Hats off.

Ready to take it one step futher in the Bonus Round?

Bonus Round - Deployment

Now that the app is built lets deploy it to render.com. Let's run some command in our terminal to prep our git repo, if you recall way back at the beginning we ran command git init this turned our project into a git repo. If you haven't been commiting your code, no worries we are going to do it together.

First sequence of command will stage all of our files to be commited, followed by the actual commit with a message of "CRUD API".

git add .
git commit -m "CRUD API"

Now we need to create a new repo on GitHub, so head over to your github dashboard and in the top right corner next to your avatar icon you will see a + sign select that and chose from the dropdown the New Repository option.

This will bring up the create a new repository screen we need to provide a name for the repo my suggestion would be "Kcanamar-GO-CRUD-API", but you can name it what ever you want.

Make sure it is set to public, and DO NOT Select to add a README file, the only option you will need to change is provide a repo name. Go a head and smash the Create Repository button at the bottom of the page.

This will take us to the code tab in our newly created repo, we are going to copy the bottom section of code, and paste it into our terminal.

git remote add origin {this will be you connection string}
git branch -M main
git push -u origin main

Go a head and refresh the page, and you will see you code on Github.

Moving on to Render

Create a render.com account or login to your account.

We are going to create a new web service, once you are on your dashboard look to the top of you screen and select the New + button and select Web Service this will promt you to signin to your github account, connecting your github to render, we need to do this.

Armed with our newly created github repo, we can import our repo directly from github, scroll through the options until you find your project name and click connect.

Render will ask you to give your porject a name.

Now let's chose the advanced setting to set up our environment variables Check the Docs, We need to define our enviornment variables, so in YOUR local .env we have these variables defined. We do not have to define the PORT variable as render.com will do this for us.

Select the Add Environment Variable for each of the following

KeyValueNote
DATABASE_URL{use your connection string from bit.io}
ENV"production"
GO111MODULEonCheck the Docs
GOPATH/opt/render/project/goCheck the Docs

Finally we can click create Web Service and Render will build our project, provided we did everything correct we should have a successful deployment.

You did it!

I you enjoyed this content or having any recommendations on what I could imporve or adjust please reach out to me through my portfolio.

Follow me on Twitter or connect with me through Linkedin