Build a CLI Task Manager with Go and MongoDB: A Step-by-Step Guide

Build a CLI Task Manager with Go and MongoDB: A Step-by-Step Guide

CLI Task Manager with Go and MongoDB

Introduction

In this tutorial, we will create a simple command-line interface (CLI) task manager using Go and MongoDB. This application will allow users to manage tasks by adding, listing, completing, and deleting them.

Prerequisites

Before we begin, ensure you have the following installed:

  • Go (version 1.16 or later)

  • MongoDB Atlas Cluster

  • A code editor (e.g., Visual Studio Code)

The source code for this project can be found on GitHub.

Project Structure

Create a new directory for your project and set up the following structure:

task-manager/
├── cli/
│   ├── task.go
├── database/
│   ├── database.go
├── model/
│   ├── task.go
├── main.go
├── .env
├── go.mod
└── go.sum

Step 1: Initialize the Go Module

Navigate to your project directory and initialize a new Go module:

go mod init tasker-manager

Step 2: Install Dependencies

go get github.com/urfave/cli/v2
go get github.com/joho/godotenv
go get go.mongodb.org/mongo-driver/mongo
go get github.com/gookit/color
go get github.com/jedib0t/go-pretty/v6/table

Step 3: Set Up a MongoDB Atlas Cluster

To store our tasks, we will use MongoDB Atlas. A database connection URL is required to connect the database. Follow the guide to set up a MongoDB Atlas cluster and get a connection URL.

Next, in the .env file, add the connection URL as an environment variable.

Step 4: Create the .env File

Create a .env file in the root of your project to store environment variables:

MONGODB_URI= <mongodb_URI>
MONGODB_DB_NAME= <db_name>
MONGODB_COLLECTION= <collection>

Step 5: Set Up MongoDB Connection

In database/database.go, we will set up the MongoDB connection.

package database

import (
    "context"
    "log"
    "os"

    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type Config struct {
    MongoDBURI        string
    MongoDBName       string
    MongoDBCollection string
}

var config *Config
var DB *mongo.Database

func init() {
    if err := godotenv.Load(); err != nil {
        log.Fatal(err)
    }

    config = &Config{
        MongoDBURI:        os.Getenv("MONGODB_URI"),
        MongoDBName:       os.Getenv("MONGODB_DB_NAME"),
        MongoDBCollection: os.Getenv("MONGODB_COLLECTION"),
    }
}

func GetDBCollection() *mongo.Collection {
    return DB.Collection(config.MongoDBCollection)
}

func NewDBInstance() error {
    client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(config.MongoDBURI))
    if err != nil {
        log.Fatal(err)
    }

    DB = client.Database(config.MongoDBName)
    return nil
}

In main.go, add the following code to initialize the database

package main

import (
    "go-mongodb/database"
    "log"
)

func main() {
    // Initialize MongoDB database instance
    err := database.NewDBInstance()
    if err != nil {
        log.Fatal(err)
    }

Step 6: Define the Task Model

In model/task.go, we will define the Task structure and the functions to interact with the MongoDB collection.

package model

import (
    "context"
    "errors"
    "fmt"
    "go-mongodb/database"
    "time"

    "github.com/gookit/color"
    "github.com/jedib0t/go-pretty/v6/table"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)

type Task struct {
    ID        primitive.ObjectID `bson:"_id"`
    Title     string             `bson:"title"`
    Completed bool               `bson:"completed"`
    CreatedAt time.Time          `bson:"created_at"`
    UpdatedAt time.Time          `bson:"updated_at"`
}

// Function to get all tasks
func GetAllTasks(filter interface{}) ([]bson.M, error) {
    var tasks []bson.M
    ctx := context.TODO()
    collection := database.GetDBCollection()
    cursor, err := collection.Find(ctx, filter)
    if err != nil {
        return tasks, err
    }

    for cursor.Next(ctx) {
        task := bson.M{}
        err := cursor.Decode(&task)
        if err != nil {
            return tasks, err
        }
        tasks = append(tasks, task)
    }

    if err := cursor.Err(); err != nil {
        return tasks, err
    }

    cursor.Close(ctx)

    if len(tasks) == 0 {
        return tasks, mongo.ErrNoDocuments
    }
    return tasks, nil
}

// Function to add a task
func AddTask(task Task) error {
    collection := database.GetDBCollection()
    _, err := collection.InsertOne(context.TODO(), &task)
    if err != nil {
        return err
    }
    return nil
}

// Function to complete a task
func CompleteTask(taskID string) error {
    objectID, err := primitive.ObjectIDFromHex(taskID)
    if err != nil {
        return err
    }

    collection := database.GetDBCollection()
    filter := bson.D{primitive.E{Key: "_id", Value: objectID}}
    update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "completed", Value: true}}}}

    _, err = collection.UpdateOne(context.TODO(), filter, update)
    return err
}

// Function to delete a task
func DeleteTask(taskID string) error {
    objectID, err := primitive.ObjectIDFromHex(taskID)
    if err != nil {
        return err
    }

    collection := database.GetDBCollection()
    filter := bson.D{primitive.E{Key: "_id", Value: objectID}}
    result, err := collection.DeleteOne(context.TODO(), filter)
    if err != nil {
        return err
    } else if result.DeletedCount == 0 {
        return errors.New("task not found")
    }
    return nil
}

// Function to print tasks
func PrintTasks(tasks []bson.M) {
    t := table.NewWriter()
    t.SetTitle("All Tasks")

    t.AppendHeader(table.Row{"#ID", "Title", "Completed"})
    for _, task := range tasks {
        id := task["_id"].(primitive.ObjectID).Hex()
        title := task["title"]
        completed := task["completed"]

        if task["completed"] == true {
            id = color.Green.Sprint(id)
            title = color.Green.Sprint(title)
            completed = color.Green.Sprint(completed)
        } else {
            id = color.Yellow.Sprint(id)
            title = color.Yellow.Sprint(title)
            completed = color.Yellow.Sprint(completed)
        }

        t.AppendRow(table.Row{id, title, completed})
    }

    fmt.Println(t.Render())
}

Step 7: Create the CLI Interface

In cli/task.go, we will define the CLI commands for managing tasks.

package cli

import (
    "fmt"
    "go-mongodb/model"
    "log"
    "os"
    "time"

    "github.com/gookit/color"
    "github.com/urfave/cli/v2"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

var taskApp *cli.App

func TaskCLI() {
    taskApp = &cli.App{
        Name:      "Task Manager",
        Version:   "v1.0",
        Compiled:  time.Now(),
        Usage:     "Task management tool",
        Commands: []*cli.Command{
            {
                Name:    "list",
                Aliases: []string{"ls"},
                Action: func(ctx *cli.Context) error {
                    fmt.Println("List all tasks...")
                    tasks, err := model.GetAllTasks(bson.D{{}})
                    if err != nil {
                        log.Fatal(err)
                    }
                    model.PrintTasks(tasks)
                    return nil
                },
                Subcommands: []*cli.Command{
                    {
                        Name: "completed",
                        Action: func(ctx *cli.Context) error {
                            tasks, err := model.FilterTasks(true)
                            if err != nil {
                                log.Fatal(err)
                            }
                            model.PrintTasks(tasks)
                            return nil
                        },
                    },
                    {
                        Name: "uncompleted",
                        Action: func(ctx *cli.Context) error {
                            tasks, err := model.FilterTasks(false)
                            if err != nil {
                                log.Fatal(err)
                            }
                            model.PrintTasks(tasks)
                            return nil
                        },
                    },
                },
            },
            {
                Name:    "add",
                Aliases: []string{"a"},
                Action: func(ctx *cli.Context) error {
                    if ctx.Args().Len() == 0 {
                        color.Red.Println("Please provide a task title!")
                        return nil
                    }

                    title := ctx.Args().First()
                    task := model.Task{
                        ID:        primitive.NewObjectID(),
                        Title:     title,
                        Completed: false,
                        CreatedAt: time.Now(),
                        UpdatedAt: time.Now(),
                    }

                    if err := model.AddTask(task); err != nil {
                        color.Red.Println("Error adding task", err)
                    } else {
                        color.Green.Println("Task added successfully!")
                    }
                    return nil
                },
            },
            {
                Name:    "complete",
                Aliases: []string{"cpt"},
                Action: func(ctx *cli.Context) error {
                    if ctx.Args().Len() == 0 {
                        color.Red.Println("Provide task id to complete task!")
                        return nil
                    }

                    id := ctx.Args().First()
                    if err := model.CompleteTask(id); err != nil {
                        color.Red.Println("Error completing task", err)
                    } else {
                        color.Green.Println("Task completed successfully")
                    }
                    return nil
                },
            },
            {
                Name:    "delete",
                Aliases: []string{"rm"},
                Action: func(ctx *cli.Context) error {
                    if ctx.Args().Len() == 0 {
                        color.Red.Println("Provide task id to delete task!")
                        return nil
                    }

                    id := ctx.Args().First()
                    if err := model.DeleteTask(id); err != nil {
                        color.Red.Println("Error deleting task", err)
                    } else {
                        color.Green.Println("Task deleted successfully")
                    }
                    return nil
                },
            },
        },
    }
}

func Run() error {
    if err := taskApp.Run(os.Args); err != nil {
        return err
    }
    return nil
}

In main.go, add the following code for the cli.

package main

import (
    "go-mongodb/cli"
    "go-mongodb/database"
    "log"
)

func main() {
    // Initialize MongoDB database instance
    err := database.NewDBInstance()
    if err != nil {
        log.Fatal(err)
    }

        // Start the task CLI
    cli.TaskCLI()
    if err := cli.Run(); err != nil {
        log.Fatal(err)
    }
}

Step 8: Using the CLI

You can now use the following commands to manage your tasks:

  • Add a new task:

      go run main.go add "Task Title"
    

  • List all tasks:

      go run main.go list
    

  • Complete a task:

      go run main.go complete <task_id>
    

  • Delete a task:

      go run main.go delete <task_id>
    

Conclusion

Congratulations! You have successfully built a CLI task manager using Go and MongoDB. You can extend this application by adding more features such as task prioritization, due dates, or user authentication.