Here is our second part of the Golang for Web series. In this article, we will see how we can add database to our app to store our todos.
Before that if you didn't follow the first part of this series, check that here.
If you've done that, let's dive into our code.
What will we build? 🤔
We will continue our Todo application from our first part of this series. Here we will add Mongo DB
as our database to store our todos.
Prerequisites 📝
- A basic knowledge of Golang syntax.
- Go version 1.14 or above installed on your machine. Install from here
- Postman or any other related app installed on your machine. Download Postman
- Part-I of this project. Clone the project from here
If you have these, Let's get started 🚀
Let’s Begin 🏁
1. Setup our Project
First, Let's open our previous project in our VS Code (or any other Code Editor/ IDE).
Now, run the below command to install all the dependencies.
go get
Let's install mongo-driver
for Mongo DB
and godotenv
to manage environment variables.
go get -u go.mongodb.org/mongo-driver github.com/joho/godotenv
Now, create models
, config
directories, and .env
file in our root directory.
Our root directory will look like this:
.
|____config
|____controllers
| |____todo.go
|____models
|____routes
| |____todo.go
|____.env
|____go.mod
|____go.sum
|____main.go
2. Setup Mongo DB
Let's create an account at https://www.mongodb.com/ or you can sign in to your existing account.
Now create a new project, named gofiber-todo-api
(or anything you want) and build a cluster.
Wait some time, while mongo db is creating a cluster for you.
Now click on connect
Then, add a database user and choose Allow Acess from Anywhere under Add a connection IP address and click on Add IP Address
Then choose Connect your application under connection method. Choose Go from the driver dropdown and 1.4 or later from the version dropdown.
Now copy that connection string. and paste it in .env
file like below.
MONGO_URI=mongodb+srv://<dbUser>:<password>@cluster0.oynlp.mongodb.net/<dbname>?retryWrites=true&w=majority
Here replace <dbUser>
with your database username, <password>
with your user password, and <dbname>
with a database name.
Additionally, add another two variables in .env
file as below.
DATABASE_NAME=<dbname>
TODO_COLLECTION=todos
Here DATABASE_NAME
is same as <dbname>
in the MONGO_URI
.
3. Configure Mongo DB
Let's create a db.go
file inside config
directory.
Now declare the package and import our required imports as below.
config/db.go
will look like :
package config
import (
"context"
"log"
"os"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
Then add MongoInstance
as below.
// MongoInstance : MongoInstance Struct
type MongoInstance struct {
Client *mongo.Client
DB *mongo.Database
}
// MI : An instance of MongoInstance Struct
var MI MongoInstance
config/db.go
will look like :
package config
import (
"context"
"fmt"
"log"
"os"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
// MongoInstance : MongoInstance Struct
type MongoInstance struct {
Client *mongo.Client
DB *mongo.Database
}
// MI : An instance of MongoInstance Struct
var MI MongoInstance
Now let's create a ConnectDB()
method with all the connection configuration.
// ConnectDB - database connection
func ConnectDB() {
client, err := mongo.NewClient(options.Client().ApplyURI(os.Getenv("MONGO_URI")))
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
err = client.Ping(ctx, readpref.Primary())
if err != nil {
log.Fatal(err)
}
fmt.Println("Database connected!")
MI = MongoInstance{
Client: client,
DB: client.Database(os.Getenv("DATABASE_NAME")),
}
}
Let's go to main.go
and import the following packages
import (
"log" // new
"github.com/devsmranjan/golang-fiber-basic-todo-app/config" // new
"github.com/devsmranjan/golang-fiber-basic-todo-app/routes"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/joho/godotenv" // new
)
Inside the main()
initiate dotenv
like below.
// dotenv
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
and call the db configuration method ConnectDB()
// config db
config.ConnectDB()
Now our main()
will look like :
func main() {
app := fiber.New()
app.Use(logger.New())
// dotenv
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
// config db
config.ConnectDB()
// setup routes
setupRoutes(app)
// Listen on server 8000 and catch error if any
err = app.Listen(":8000")
// handle error
if err != nil {
panic(err)
}
}
Now let's run our app
go run main.go
You will get output like below
Database connected!
┌───────────────────────────────────────────────────┐
│ Fiber v2.2.0 │
│ http://127.0.0.1:8000 │
│ │
│ Handlers ............ 12 Threads ............. 4 │
│ Prefork ....... Disabled PID ............. 55680 │
└───────────────────────────────────────────────────┘
It means our database successfully connected.
Additional: To reload the server automatically, you can install air globally in your machine.
Now to run our app, in your root directory run
air
or for debug mode, you can run
air -d
4. Create Model
Now create a file named todo.go
inside models
directory.
And create our Todo
like below
package models
import (
"time"
)
// Todo - todo model
type Todo struct {
ID *string `json:"id,omitempty" bson:"_id,omitempty"`
Title *string `json:"title"`
Completed *bool `json:"completed"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
5. Connect Database with Controllers
Now go to controllers/todo.go
and remove the following code first.
- // Todo : todo model
- type Todo struct {
- ID int `json:"id"`
- Title string `json:"title"`
- Completed bool `json:"completed"`
- }
-
- var todos = []*Todo{
- {
- ID: 1,
- Title: "Walk the dog 🦮",
- Completed: false,
- },
- {
- ID: 2,
- Title: "Walk the cat 🐈",
- Completed: false,
- },
- }
And let's import required packages
import (
"os" // new
"strconv"
"github.com/devsmranjan/golang-fiber-basic-todo-app/config" // new
"github.com/devsmranjan/golang-fiber-basic-todo-app/models" // new
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson" // new
"go.mongodb.org/mongo-driver/bson/primitive" // new
"go.mongodb.org/mongo-driver/mongo" // new
)
5.1 - GetTodos()
+ todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
+
+ // Query to filter
+ query := bson.D{{}}
+
+ cursor, err := todoCollection.Find(c.Context(), query)
+
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "success": false,
+ "message": "Something went wrong",
+ "error": err.Error(),
+ })
+ }
+
+ var todos []models.Todo = make([]models.Todo, 0)
+
+ // iterate the cursor and decode each item into a Todo
+ err = cursor.All(c.Context(), &todos)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "success": false,
+ "message": "Something went wrong",
+ "error": err.Error(),
+ })
+ }
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todos": todos,
},
})
Now the GetTodos()
will look like:
// GetTodos : get all todos
func GetTodos(c *fiber.Ctx) error {
todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// Query to filter
query := bson.D{{}}
cursor, err := todoCollection.Find(c.Context(), query)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": "Something went wrong",
"error": err.Error(),
})
}
var todos []models.Todo = make([]models.Todo, 0)
// iterate the cursor and decode each item into a Todo
err = cursor.All(c.Context(), &todos)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": "Something went wrong",
"error": err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todos": todos,
},
})
}
5.2 - CreateTodo()
+ todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
+
- type Request struct {
- Title string `json:"title"`
- }
-
- var data Request
+
+ data := new(models.Todo)
err := c.BodyParser(&body)
// if error
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse JSON",
"error": err,
})
}
-
- // create a todo variable
- todo := &Todo{
- ID: len(todos) + 1,
- Title: body.Title,
- Completed: false,
- }
-
- // append in todos
- todos = append(todos, todo)
-
+ data.ID = nil
+ f := false
+ data.Completed = &f
+ data.CreatedAt = time.Now()
+ data.UpdatedAt = time.Now()
+
+ result, err := todoCollection.InsertOne(c.Context(), data)
+
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "success": false,
+ "message": "Cannot insert todo",
+ "error": err,
+ })
+ }
+
+ // get the inserted data
+ todo := &models.Todo{}
+ query := bson.D{{Key: "_id", Value: result.InsertedID}}
+
+ todoCollection.FindOne(c.Context(), query).Decode(todo)
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todo": todo,
},
})
Now CreateTodo()
will look like:
// CreateTodo : Create a todo
func CreateTodo(c *fiber.Ctx) error {
todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
data := new(models.Todo)
err := c.BodyParser(&data)
// if error
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse JSON",
"error": err,
})
}
data.ID = nil
f := false
data.Completed = &f
data.CreatedAt = time.Now()
data.UpdatedAt = time.Now()
result, err := todoCollection.InsertOne(c.Context(), data)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot insert todo",
"error": err,
})
}
// get the inserted data
todo := &models.Todo{}
query := bson.D{{Key: "_id", Value: result.InsertedID}}
todoCollection.FindOne(c.Context(), query).Decode(todo)
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todo": todo,
},
})
}
5.3 - GetTodo()
+ todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// get parameter value
paramID := c.Params("id")
// convert parameter value string to int
- id, err := strconv.Atoi(paramID)
+ id, err := primitive.ObjectIDFromHex(paramID)
// if error in parsing string to int
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse Id",
"error": err,
})
}
// find todo and return
- for _, todo := range todos {
- if todo.ID == id {
- return c.Status(fiber.StatusOK).JSON(fiber.Map{
- "success": true,
- "data": fiber.Map{
- "todo": todo,
- },
- })
- }
- }
-
- // if todo not available
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
- "success": false,
- "message": "Todo not found",
- })
+
+ todo := &models.Todo{}
+
+ query := bson.D{{Key: "_id", Value: id}}
+
+ err = todoCollection.FindOne(c.Context(), query).Decode(todo)
+
+ if err != nil {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
+ "success": false,
+ "message": "Todo Not found",
+ "error": err,
+ })
+ }
+
+ return c.Status(fiber.StatusOK).JSON(fiber.Map{
+ "success": true,
+ "data": fiber.Map{
+ "todo": todo,
+ },
+ })
Now GetTodo()
will look like:
// GetTodo : get a single todo
// PARAM: id
func GetTodo(c *fiber.Ctx) error {
todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// get parameter value
paramID := c.Params("id")
// convert parameterID to objectId
id, err := primitive.ObjectIDFromHex(paramID)
// if error while parsing paramID
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse Id",
"error": err,
})
}
// find todo and return
todo := &models.Todo{}
query := bson.D{{Key: "_id", Value: id}}
err = todoCollection.FindOne(c.Context(), query).Decode(todo)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"message": "Todo Not found",
"error": err,
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todo": todo,
},
})
}
5.4 - UpdateTodo()
+ todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// find parameter
paramID := c.Params("id")
// convert parameter string to int
- id, err := strconv.Atoi(paramID)
// if parameter cannot parse
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse id",
"error": err,
})
}
-
- // request structure
- type Request struct {
- Title *string `json:"title"`
- Completed *bool `json:"completed"`
- }
var data Request
err = c.BodyParser(&data)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse JSON",
"error": err,
})
}
-
- var todo *Todo
-
- for _, t := range todos {
- if t.ID == id {
- todo = t
- break
- }
- }
-
- if todo.ID == 0 {
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
- "success": false,
- "message": "Not found",
- })
- }
+
+ query := bson.D{{Key: "_id", Value: id}}
+
+ // updateData
+ var dataToUpdate bson.D
if data.Title != nil {
- todo.Title = *data.Title
+ dataToUpdate = append(dataToUpdate, bson.E{Key: "title", Value: data.Title})
}
if data.Completed != nil {
- todo.Completed = *data.Completed
+ dataToUpdate = append(dataToUpdate, bson.E{Key: "completed", Value: data.Completed})
}
+
+ dataToUpdate = append(dataToUpdate, bson.E{Key: "updatedAt", Value: time.Now()})
+
+ update := bson.D{
+ {Key: "$set", Value: dataToUpdate},
+ }
+
+ // update
+ err = todoCollection.FindOneAndUpdate(c.Context(), query, update).Err()
+
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
+ "success": false,
+ "message": "Todo Not found",
+ "error": err,
+ })
+ }
+
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "success": false,
+ "message": "Cannot update todo",
+ "error": err,
+ })
+ }
+
+ // get updated data
+ todo := &models.Todo{}
+
+ todoCollection.FindOne(c.Context(), query).Decode(todo)
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todo": todo,
},
})
Now UpdateTodo()
will look like:
// UpdateTodo : Update a todo
// PARAM: id
func UpdateTodo(c *fiber.Ctx) error {
todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// find parameter
paramID := c.Params("id")
// convert parameterID to objectId
id, err := primitive.ObjectIDFromHex(paramID)
// if parameter cannot parse
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse id",
"error": err,
})
}
// var data Request
data := new(models.Todo)
err = c.BodyParser(&data)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse JSON",
"error": err,
})
}
query := bson.D{{Key: "_id", Value: id}}
// updateData
var dataToUpdate bson.D
if data.Title != nil {
// todo.Title = *data.Title
dataToUpdate = append(dataToUpdate, bson.E{Key: "title", Value: data.Title})
}
if data.Completed != nil {
// todo.Completed = *data.Completed
dataToUpdate = append(dataToUpdate, bson.E{Key: "completed", Value: data.Completed})
}
dataToUpdate = append(dataToUpdate, bson.E{Key: "updatedAt", Value: time.Now()})
update := bson.D{
{Key: "$set", Value: dataToUpdate},
}
// update
err = todoCollection.FindOneAndUpdate(c.Context(), query, update).Err()
if err != nil {
if err == mongo.ErrNoDocuments {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"message": "Todo Not found",
"error": err,
})
}
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot update todo",
"error": err,
})
}
// get updated data
todo := &models.Todo{}
todoCollection.FindOne(c.Context(), query).Decode(todo)
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"success": true,
"data": fiber.Map{
"todo": todo,
},
})
}
5.5 - DeleteTodo()
+ todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// get param
paramID := c.Params("id")
-
- // convert param string to int
+ // convert parameter to object id
- id, err := strconv.Atoi(paramID)
+ id, err := primitive.ObjectIDFromHex(paramID)
// if parameter cannot parse
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse id",
"error": err,
})
}
// find and delete todo
- for i, todo := range todos {
- if todo.ID == id {
-
- todos = append(todos[:i], todos[i+1:]...)
-
- return c.SendStatus(fiber.StatusNoContent)
- }
- }
+
+ query := bson.D{{Key: "_id", Value: id}}
+
+ err = todoCollection.FindOneAndDelete(c.Context(), query).Err()
+
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
+ "success": false,
+ "message": "Todo Not found",
+ "error": err,
+ })
+ }
+
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "success": false,
+ "message": "Cannot delete todo",
+ "error": err,
+ })
+ }
-
- // if todo not found
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
- "success": false,
- "message": "Todo not found",
- })
+
+ return c.SendStatus(fiber.StatusNoContent)
Now DeleteTodo()
will look like:
// DeleteTodo : Delete a todo
// PARAM: id
func DeleteTodo(c *fiber.Ctx) error {
todoCollection := config.MI.DB.Collection(os.Getenv("TODO_COLLECTION"))
// get param
paramID := c.Params("id")
// convert parameter to object id
id, err := primitive.ObjectIDFromHex(paramID)
// if parameter cannot parse
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot parse id",
"error": err,
})
}
// find and delete todo
query := bson.D{{Key: "_id", Value: id}}
err = todoCollection.FindOneAndDelete(c.Context(), query).Err()
if err != nil {
if err == mongo.ErrNoDocuments {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"message": "Todo Not found",
"error": err,
})
}
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "Cannot delete todo",
"error": err,
})
}
return c.SendStatus(fiber.StatusNoContent)
}
Awesome !!! We did great so far. 🤝
6. Test our endpoints 🧪
Let's test our API in Postman. Before that don't forget to run the server.
To get all Todos, give a GET
request to localhost:8000/api/todos
To create a new todo, give a POST
request to localhost:8000/api/todos
with a title: <String>
in the request body.
Now, you can add more todos like:
- Play Cricket 🏏
- Watch WandaVision 🍿
Let's get all our todos again.
Yaaahooooo! Isn't this amazing? 🤩
Now, to get todo by id, give a GET
request to localhost:8000/api/todos/:id
Here replace :id
with a todo id
.
Now, let's update a todo by giving a PUT
request to localhost:8000/api/todos/:id
with a title: <String>
or completed: <Boolean>
or both in the request body.
Here replace :id
with a todo id
.
To delete a todo, give a DELETE
request to localhost:8000/api/todos/:id
Here replace :id
with a todo id
.
Congratulations 🥳 🥳 🥳 We did it 💪🏻
Conclusion 📋
For more information about gofiber, I suggest taking a deeper look at the documentation here https://docs.gofiber.io/
Mongo DB official documentation.
Here is my GitHub link to this project - https://github.com/devsmranjan/golang-fiber-basic-todo-app/
Thank you for reading my article 🙂 . I hope you have learned something here.
If you want to improve this project, fork the repo here.
Happy coding 👨💻👩💻 and stay tuned for my next post in this series!
Thanks! Don't forget to give a ♥️ and follow :)
Top comments (3)
very good tutorial , but there is a change , while creating
type Todo struct {
json:"id,omitempty" bson:"_id,omitempty"ID *string
json:"title"Title *string
json:"completed"Completed *bool
json:"createdAt"CreatedAt time.Time
json:"updatedAt"UpdatedAt time.Time
}
at the backend , mongo db reads it as createdAt, updated at (by making it all small letters)-> i have checked this in atlas, so we can put
type Todo struct {
json:"id,omitempty" bson:"_id,omitempty"Id *string
json:"title" bson:"title"Title *string
json:"completed" bson:"completed"Completed *bool
json:"createdAt" bson:"createdAt"CreatedAt time.Time
json:"updatedAt" bson:"updatedAt"UpdateAt time.Time
}
this case problem occurs when you are editing , because at the update function we are using
dataToUpdate = append(dataToUpdate, bson.E{Key: "updatedAt", Value: time.Now()})
, this will led to error(creates a new field "updateAt") if we dont manually give as bsonLovely tutorials, you've brought me closer to understanding Go!
Awesome, thankyou
Ummm, could you make the Gin tutorial?