Building a Task Manager using Go programming language and MongoDB Database

Building a Task Manager using Go programming language and MongoDB Database

In this article we are going to be doing something new, we are creating a simple task manager using the Go programming language and a MongoDB database.

Why Go:

Go or Golang as it is infamously known for is a statically typed, compiled, open-source language developed by Google, it was developed to be simple, high performing, readable and efficient. It has similar syntax with C, but less memory usage, It has good garbage collection and excellent things have been said about its concurrency.

About the application:

We will be building a task manager application that has the ability to read, write, delete and update tasks into a MongoDB database. We will be using Chi for routing.

Prerequisites

  1. Basic knowledge of Go and already installed
  2. MongoDB installed

Lets Begin:

First of all, we need to create a directory for the project at the src folder under Go

C:\users\user\go\src> mkdir task-manager

Next, we initialize the directory, it is like creating a package.json file for our nodejs readers.

C:\users\user\go\src> cd task-manger
C:\users\user\go\src\task-manager> go mod init github.com/yourusername/task-manager

The packages we are going to use for this project include:

  • Chi: is a lightweight, idiomatic and composable router for building Go HTTP services.
  • mgo: This is the MongoDB driver for Go.
  • Renderer: Simple, lightweight and faster response (JSON, JSONP, XML, YAML, HTML, File) rendering package for Go
go get "github.com/go-chi/chi"
go get "gopkg.in/mgo.v2"
go get "github.com/thedevsaddam/renderer"

After installing the packages we move into our preferred code editor I will be using vs code this can be achieved by using code . right from the terminal. create your main.go file and inside it, the first thing you write is the package main. then you import the various packages we will use both the core and third-party packages. it should look something like this

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "time"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    mgo "gopkg.in/mgo.v2"
    "github.com/thedevsaddam/renderer"
    "gopkg.in/mgo.v2/bson"
)
var rnd *renderer.Render
var db *mgo.Database

Go has this amazing habit of creating errors on unused packages so as to save memory so do not worry if your IDE lights up with errors they will gradually leave whenever we use the said package. Next, we create our set of constants that will be used throughout the app. they include things like hostnames, port, DB collection names, etc. we will be accessing our application on localhost:9000

const (
    hostName       string = "localhost:27017"
    dbName         string = "demo_task"
    collectionName string = "task"
    port           string = ":9000"
)

now we will create our structs to use in our database, there are two structs, one is in BSON which will be used to interact with our database, the JSON will be used to interact with our front end.

type (
    taskModel struct {
        ID        bson.ObjectId `bson:"_id,omitempty"`
        Title     string        `bson:"title"`
        Completed bool          `bson:"completed"`
        CreatedAt time.Time     `bson:"createdAt"`
    }
    task struct {
        ID        string    `json:"id"`
        Title     string    `json:"title"`
        Completed bool      `json:"completed"`
        CreatedAt time.Time `json:"created_at"`
    }
)

the schema used in the application above is essential for our task manager application for we need the ID to access individual tasks to either update or delete. Next, we want to create a database and start a session and we can do that using a function called init()

func init() {
    rnd = renderer.New()
    sess, err := mgo.Dial(hostName)
    checkErr(err)
    sess.SetMode(mgo.Monotonic, true)
    db = sess.DB(dbName)
}

notice the checkErr() function we will have to create that so we can log out any err that occurs when trying to connect to our MongoDB database

func checkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

you will notice no more error under log in our imports. Now comes the main part where we have to define our main function. func main is the entry point to all go applications, inside the function we will create a router and specify our routes.

func main() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Get("/", homeHandler)
    r.Mount("/task", taskHandlers())
}

we want to route the application home / to trigger the homeHandler function and all subsequent /task mounted by our taskhandlers func.

func taskHandlers() http.Handler {
    rg := chi.NewRouter()
    rg.Group(func(r chi.Router) {
        r.Get("/", fetchTask)
        r.Post("/", createTask)
        r.Put("/{id}", updateTask)
        r.Delete("/{id}", deleteTask)
    })
    return rg
}

back into our func main we will be creating our servers and a channel called go func which will help us start our servers and stop a server gracefully(it is a pretty advanced code, we could stop a server by using ctl c but it is always advisable to follow best practices). our updated func main looks like

func main() {
    stopChan := make(chan os.Signal)
    signal.Notify(stopChan, os.Interrupt)
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Get("/", homeHandler)
    r.Mount("/task", taskHandlers())

    srv := &http.Server{
        Addr:         port,
        Handler:      r,
        ReadTimeout:  60 * time.Second,
        WriteTimeout: 60 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        log.Println("listening on port", port)
        if err := srv.ListenAndServe(); err != nil {
            log.Printf("listen:%s\n", err)
        }
    }()
    <-stopChan
    log.Println("Shutting down server ...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    srv.Shutdown(ctx)
    defer cancel()
    log.Println("Server gracefully stopped")
}

Now we will be creating our homeHandler to help serve our front end. our front end will be served on a static folder which should be created. We will not be creating the front-end in this tutorial but you can access the files here on my Github where you can download or clone the repo

func homeHandler(w http.ResponseWriter, r *http.Request) {
    err := rnd.Template(w, http.StatusOK, []string{"static/home.tpl"}, nil)
    checkErr(err)
}

when that's done we can now focus on creating our taskHandlers function. the first on our list is a fetchTask function which presents the list of all our available tasks in our app, it finds the tasks on the database convert them from BSON to JSON and then render it into the application

func fetchTask(w http.ResponseWriter, r *http.Request) {
    tasks := []taskModel{}
    if err := db.C(collectionName).Find(bson.M{}).All(&tasks); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to fetch Tasks",
            "error":   err,
        })
        return
    }
    taskList := []task{}

    for _, t := range tasks {
        taskList = append(taskList, task{
            ID:        t.ID.Hex(),
            Title:     t.Title,
            Completed: t.Completed,
            CreatedAt: t.CreatedAt,
        })
    }
    rnd.JSON(w, http.StatusOK, renderer.M{
        "data": taskList,
    })

}

Next on the taskHandlers list is the create task function, there are a few steps to follow:

first we decode the data we receive from the user/front end, next we perform a simple validation to check if the user request has a Title and display an error if there is none, third we create the task model we will send into the database, then we send the model lastly we send a response back to the user saying the task to created successfully

func createTask(w http.ResponseWriter, r *http.Request) {
    var t task

    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        rnd.JSON(w, http.StatusProcessing, err)
        return
    }

    if t.Title == "" {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "The title is required",
        })
        return
    }
    tm := taskModel{
        ID:        bson.NewObjectId(),
        Title:     t.Title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    if err := db.C(collectionName).Insert(&tm); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to save Task",
            "error":   err,
        })
        return
    }

    rnd.JSON(w, http.StatusCreated, renderer.M{
        "message": "Task created succesfully",
        "task_id": tm.ID.Hex(),
    })
}

Phew, you should take a break now and check out what you have done so far, if you made it this far hey! thanks for doing this with me you are amazing. now back to work, the next on the taskHandlers list is deleteTask, like the createTask function this has some steps too:

First, we will work with the URL parameter like id and assign that to an id variable which will be used later on, since we will be using a Hex id we have to validate if it is really a hex and throw an error if it isn't, fourthly working with the database and deleting the specified task and lastly writing to the frontend displaying a response saying the task was deleted successfully

func deleteTask(w http.ResponseWriter, r *http.Request) {
    id := strings.TrimSpace(chi.URLParam(r, "id"))

    if !bson.IsObjectIdHex(id) {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "The id is invalid",
        })
        return
    }
    if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to delete Task",
            "error":   err,
        })
        return
    }

    rnd.JSON(w, http.StatusOK, renderer.M{
        "message": "Task Deleted Successfully",
    })
}

Lastly in the taskHandlers is the updateTask. the updateTask is a mixture of get by id in the deleteTask function and createTask

func updateTask(w http.ResponseWriter, r *http.Request) {
    id := strings.TrimSpace(chi.URLParam(r, "id"))

    if !bson.IsObjectIdHex(id) {
        rnd.JSON(w, http.StatusBadGateway, renderer.M{
            "message": "The id is Invalid",
        })
        return
    }

    var t task
    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        rnd.JSON(w, http.StatusProcessing, err)
        return
    }

    if t.Title == "" {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "the input filed id is required",
        })
        return
    }

    if err := db.C(collectionName).Update(
        bson.M{"_id": bson.ObjectIdHex(id)},
        bson.M{"title": t.Title, "completed": t.Completed},
    ); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to update task",
            "error":   err,
        })
        return
    }
    rnd.JSON(w, http.StatusOK, renderer.M{
        "message": "Task updated successfully",
    })
}

And that's it! before we go into building the application in the terminal remember to download the static folder and insert it into your code. Your final main.go file should look like this

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "time"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    "github.com/thedevsaddam/renderer"
    mgo "gopkg.in/mgo.v2"
    "gopkg.in/mgo.v2/bson"
)

var rnd *renderer.Render
var db *mgo.Database

const (
    hostName       string = "localhost:27017"
    dbName         string = "demo_task"
    collectionName string = "task"
    port           string = ":9000"
)

type (
    taskModel struct {
        ID        bson.ObjectId `bson:"_id,omitempty"`
        Title     string        `bson:"title"`
        Completed bool          `bson:"completed"`
        CreatedAt time.Time     `bson:"createdAt"`
    }
    task struct {
        ID        string    `json:"id"`
        Title     string    `json:"title"`
        Completed bool      `json:"completed"`
        CreatedAt time.Time `json:"created_at"`
    }
)

func init() {
    rnd = renderer.New()
    sess, err := mgo.Dial(hostName)
    checkErr(err)
    sess.SetMode(mgo.Monotonic, true)
    db = sess.DB(dbName)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
    err := rnd.Template(w, http.StatusOK, []string{"static/home.tpl"}, nil)
    checkErr(err)
}
func fetchTask(w http.ResponseWriter, r *http.Request) {
    tasks := []taskModel{}
    if err := db.C(collectionName).Find(bson.M{}).All(&tasks); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to fetch Tasks",
            "error":   err,
        })
        return
    }
    taskList := []task{}

    for _, t := range tasks {
        taskList = append(taskList, task{
            ID:        t.ID.Hex(),
            Title:     t.Title,
            Completed: t.Completed,
            CreatedAt: t.CreatedAt,
        })
    }
    rnd.JSON(w, http.StatusOK, renderer.M{
        "data": taskList,
    })

}

func createTask(w http.ResponseWriter, r *http.Request) {
    var t task

    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        rnd.JSON(w, http.StatusProcessing, err)
        return
    }

    if t.Title == "" {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "The title is required",
        })
        return
    }
    tm := taskModel{
        ID:        bson.NewObjectId(),
        Title:     t.Title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    if err := db.C(collectionName).Insert(&tm); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to save Task",
            "error":   err,
        })
        return
    }

    rnd.JSON(w, http.StatusCreated, renderer.M{
        "message": "Task created succesfully",
        "task_id": tm.ID.Hex(),
    })
}

func deleteTask(w http.ResponseWriter, r *http.Request) {
    id := strings.TrimSpace(chi.URLParam(r, "id"))

    if !bson.IsObjectIdHex(id) {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "The id is invalid",
        })
        return
    }
    if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to delete Task",
            "error":   err,
        })
        return
    }

    rnd.JSON(w, http.StatusOK, renderer.M{
        "message": "Task Deleted Successfully",
    })
}
func updateTask(w http.ResponseWriter, r *http.Request) {
    id := strings.TrimSpace(chi.URLParam(r, "id"))

    if !bson.IsObjectIdHex(id) {
        rnd.JSON(w, http.StatusBadGateway, renderer.M{
            "message": "The id is Invalid",
        })
        return
    }

    var t task
    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        rnd.JSON(w, http.StatusProcessing, err)
        return
    }

    if t.Title == "" {
        rnd.JSON(w, http.StatusBadRequest, renderer.M{
            "message": "the input filed id is required",
        })
        return
    }

    if err := db.C(collectionName).Update(
        bson.M{"_id": bson.ObjectIdHex(id)},
        bson.M{"title": t.Title, "completed": t.Completed},
    ); err != nil {
        rnd.JSON(w, http.StatusProcessing, renderer.M{
            "message": "Failed to update task",
            "error":   err,
        })
        return
    }
    rnd.JSON(w, http.StatusOK, renderer.M{
        "message": "Task updated successfully",
    })
}

func main() {
    stopChan := make(chan os.Signal)
    signal.Notify(stopChan, os.Interrupt)
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Get("/", homeHandler)
    r.Mount("/task", taskHandlers())

    srv := &http.Server{
        Addr:         port,
        Handler:      r,
        ReadTimeout:  60 * time.Second,
        WriteTimeout: 60 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        log.Println("listening on port", port)
        if err := srv.ListenAndServe(); err != nil {
            log.Printf("listen:%s\n", err)
        }
    }()
    <-stopChan
    log.Println("Shutting down server ...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    srv.Shutdown(ctx)
    defer cancel()
    log.Println("Server gracefully stopped")
}

func taskHandlers() http.Handler {
    rg := chi.NewRouter()
    rg.Group(func(r chi.Router) {
        r.Get("/", fetchTask)
        r.Post("/", createTask)
        r.Put("/{id}", updateTask)
        r.Delete("/{id}", deleteTask)
    })
    return rg
}

func checkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

Final steps:

In your terminal we are going to be building the app which will compile the code and create a binary executable file before running it we do that by use the go build command on the project directory

C:\users\user\go\src\task-manager> go build

It will bring up any errors you may have made during typing you should fix it and run go build again. In the absence of errors, you can run

C:\users\user\go\src\task-manager> go run main.go

After some seconds you should see this:

2022/02/08 19:06:44 listening on port:9000

You may get a pop up from firewall make sure you allow access and head over to localhost:9000 in your browser you will be greeted with your application up and running do have fun with it.

Screenshot (54).png

Remember to leave a like, comment, and share if you found this tutorial helpful. Cheers!