Go Bun in action: Adding Database Support to Task Manager REST API
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:
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.
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:
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:
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:
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)
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:
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.