Go Bun in action: Adding Database Support to Task Manager REST API

Zahid
7 min readJun 11, 2023

--

Background:

In previous post, we set up our project and created a basic REST API using Go and Gin. Following that, we developed a Task Manager REST API project within that setup.

If you haven’t read the previous post yet, you can find it here:

While building the API, we encountered a limitation — whenever we closed our Go server, our data would be lost. This was because we were using in-memory slices for data storage, which did not provide persistence. In this article, we will address this issue by adding persistence to our Task Manager API project.

This was our code:

Task Manager API with in-memory slice storage

To enable data persistence in our API, we’ll utilize PostgreSQL and the Go bun ORM.

Let’s explore how we can seamlessly integrate PostgreSQL and Go bun to achieve reliable data persistence in our Task Manager API.

Setup Instructions:

1. Create a Database

Run the following command to create a database named task_manager_api_with_bun:

 psql -U postgres -c 'create database task_manager_api_with_bun;'

2. Initialize the Module

Initialize the Go module for your project using the following command:

go mod init github.com/zaahidali/task_manager_api_with_bun

3. Import the Required Packages:

In your Go code, import the necessary packages for your project, including Go Gin, Go Bun, and other related packages. Here is an example import statement:

import ( 
"database/sql"
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/extra/bundebug"
)

4. Install the dependencies

Install the project dependencies and tidy up the Go module using the following commands:

go get
go mod tidy

These steps will help you set up the necessary environment and dependencies for your task manager API with Go Gin and Go Bun.

5. Setting up the bun Connection with PostgreSQL

In the provided code, we establish a connection to a PostgreSQL database using the given connection details. If the “tasks” table does not exist in the database, we create it.

Setting up the bun Connection with PostgreSQL in Go

Now, we will proceed with updating the code for the handlers step by step.

6. Migrating to Go bun: From In-Memory Slices to PostgreSQL Database

In this section, we will cover the migration process. We’ll explore how to transition your code from using in-memory slices for memory storage to utilizing Go bun for seamless interaction with the PostgreSQL database.

Step-by-step instructions and subheadings will guide you through the migration, ensuring a smooth transition.

API Endpoints:

  • GET /tasks: Retrieve a list of all tasks.
  • GET /tasks/:id: Fetch the details of a specific task.
  • PUT /tasks/:id: Update a specific task. This endpoint accepts a JSON body containing the new details of the task.
  • DELETE /tasks/:id: Remove a specific task.
  • POST /tasks: Create a new task. This endpoint accepts a JSON body with the task’s title, description, due date, and status.

let’s get started!

1. GET all tasks

func getTasks(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"tasks": tasks})
}

transforms into:

Fetch all tasks using bun

In this updated code, we have enhanced the task retrieval process by utilizing Go bun and the NewSelect query builder. The code executes a query to fetch tasks from the database and stores them in a slice. If any errors occur during the retrieval process, an appropriate error response is returned.

Finally, the retrieved tasks are sent back as a JSON response with a status code of 200 (OK). This code enables efficient retrieval of tasks from a database in a web application.

2. Getting a Specific Task (GET /tasks/:id)

func getTask(ctx *gin.Context) {
id := ctx.Param("id")

for _, val := range tasks {
if val.ID == id {
ctx.JSON(http.StatusOK, val)
return
}
}

ctx.JSON(http.StatusNotFound, gin.H{"message": "Task not found"})
}

can be updated as:

Get specific task using id

In this code, we retrieve a specific task by its ID using .NewSelect() in Gin. We check if the task ID is empty and return an error if it is. If the task is found, we return the task details as a JSON response. If the task is not found, we return a "Task not found" message.

4. Updating a Specific Task (PUT /tasks/:id)

func updateTask(ctx *gin.Context) {
id := ctx.Param("id")

var updatedTask Task

if err := ctx.ShouldBindJSON(&updatedTask); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

for i, task := range tasks {
if task.ID == id {
if updatedTask.Title != "" {
tasks[i].Title = updatedTask.Title
}
if updatedTask.Description != "" {
tasks[i].Description = updatedTask.Description
}
ctx.JSON(http.StatusOK, gin.H{"message": "Task updated"})
return
}
}

ctx.JSON(http.StatusNotFound, gin.H{"message": "Task not found"})
}

will transform into:

Update Task by ID

In the updateTask function, we handle the task update operation by retrieving the task ID from the request parameters. We verify the presence of the ID and bind the JSON body to the updatedTask struct. Using Go bun's NewUpdate method, we update the task record in the database. The function returns appropriate responses for errors and successful updates.

5. Deleting a Specific Task (DELETE /tasks/:id)

func getTask(ctx *gin.Context) {
id := ctx.Param("id")

for _, val := range tasks {
if val.ID == id {
ctx.JSON(http.StatusOK, val)
return
}
}

ctx.JSON(http.StatusNotFound, gin.H{"message": "Task not found"})
}

will transform into this:

Similarly, in the removeTask function, we handle the task deletion operation by retrieving the task ID from the request parameters. We verify the presence of the ID and use Go bun’s NewDelete method to delete the task record from the database. The function returns appropriate responses for errors and successful deletions.

6. Creating Tasks (POST /tasks)

func addTask(ctx *gin.Context) {
var newTask Task

if err := ctx.ShouldBindJSON(&newTask); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

tasks = append(tasks, newTask)
ctx.JSON(http.StatusCreated, gin.H{"message": "Task created"})
}

will become this:

In the addTask function, we handle the task creation operation by retrieving the task details from the request body. We bind the JSON data to the addTask struct and utilize Go bun's NewInsert method to create the task record in the database. The function provides suitable responses for both error scenarios and successful task creation.

7. Testing the API endpoints with Postman

To test the API endpoints using Postman, we can follow a similar approach to what was discussed in the previous article. You can refer to the section on Testing API Endpoints with Postman for detailed instructions.

To get a comprehensive view of our code, let’s examine how all the implemented code will come together in main.go file.

Now, let’s delve into the code refactoring process.

8. Refactoring the code

Let’s see how our project will be structured:

task_manager_api_with_bun
├── handlers
│ └── tasks_handler.go
├── model
│ └── task.go
├── go.mod
└── main.go

We will create a task.go file in the model folder to house the Task struct code, promoting better code organization.

So, let’s move the struct code to model/task.go. The updated code will appear as follows:

Final Code — model/task.go

package model

import "time"

type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
DueDate time.Time `json:"due_date"`
Status string `json:"status"`
}

Now, create a handlers folder and inside it, create a task_handlers.go file and their final code is given below:

Final code — (handlers/task_handlers.go)

Moved task_handlers code to handlers/task_handlers.go

In the above code in our handlers package, we imported our model package using the following import statement:

"github.com/zaahidali/task_manager_api_with_bun/model"

By importing the model package, we gain access to the Task struct and other functionalities defined in that package.

To enhance visibility and consistency, we have renamed the methods in the handlers package to start with a capital letter. For example, addTask has been renamed to AddTask. This naming convention improves code readability and makes the methods easily accessible across packages.

P.S. We also updated the handlers’ code to utilize model.Task for improved consistency and clarity. Now, model.Task is used whenever referencing the Task struct, ensuring a unified approach throughout the codebase.

Next, in the main.go file, we have implemented separate methods for establishing a database connection and creating the tasks table:

func connectToDatabase() (*bun.DB, error) {
dsn := "postgres://postgres:postgres@localhost:5432/task_manager_api_with_bun?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
return db, nil
}

func createTasksTable(db *bun.DB) error {
ctx := context.Background()
_, err := db.NewCreateTable().Model((*model.Task)(nil)).IfNotExists().Exec(ctx)
return err
}

Similarly, we have imported these packages in the main.go file in order to access its content:

 "github.com/zaahidali/task_manager_api_with_bun/handlers"
"github.com/zaahidali/task_manager_api_with_bun/model"

Final code — main.go

Our final main.go file look like this:

Refactored main.go file

In the provided code, we import the required packages, including model and handlers. These packages contain the necessary functions and methods for handling different API endpoints. The line handlers.DB = db assigns the db object to the DB variable in the handlers package. This assignment allows the handlers to access and utilize the database connection when executing queries and performing various database operations. By establishing this connection, the handlers can seamlessly interact with the database throughout the program's execution.

To ensure the proper management of resources, the code includes the defer db.Close() statement. This statement ensures that the database connection is correctly closed and resources are released when the program execution ends. By deferring the closure of the connection, we guarantee that it will be closed regardless of how the program exits, helping prevent resource leaks and maintaining the integrity of the system.

Finally! Our REST API now seamlessly persists data using Go bun and a PostgreSQL database, enhancing its functionality and reliability.

let’s end it with a beautiful quote:

“You don’t have to be great to start, but you have to start to be great.” — Zig Ziglar

You can find the final code at the following location:

Final Notes and Further Improvement Suggestions:

  • Improve the code by adding auto-increment or UUID generation for the ID field instead of using a string, to enhance uniqueness and identification of tasks.
  • Encapsulate the DB variable (*bun.DB) instead of using it as a global variable, and pass it as an argument to promote better code organization and maintainability.
  • Store sensitive information, such as the database connection string, in a separate .env file or through environment variables for improved security and flexibility.
  • In future posts, we will cover these with implementation of associations between models and explore GraphQL (using gqlgen) to further improve your knowledge with other projects.

--

--

Zahid
Zahid

Written by Zahid

Full Stack Software Engineer | Active Learner

No responses yet