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.
Step 1: Project setup and creating a module
Before we write any code, you’ll need to create a task-manager
directory on your computer to act as the top-level ‘home’ for this project.
Open your terminal and create a new project directory called task-manager
anywhere on your computer. I will locate my project directory under $HOME/task-manager
, you can use a different location if you wish.
$ mkdir task-manager
Creating a module
First, ensure that you’re in the root of your project directory and then run the go mod init
command — passing in your chosen module path as a parameter like so:
$ cd task-manager
$ go mod init task-manager
go: creating new go.mod: module task-manager
At this point, your project directory should look like the structure below. Notice the go.mod
file that has been created?
task-manager/
└── go.mod
Now let's implement the outline structure for the project. Run the following commands in the root directory of the project.
$ mkdir -p cli database model
$ touch cli/task.go
$ touch database/database.go
$ touch model/task.go
$ touch .env
$ touch main.go
The structure of your project directory should now look like this:
task-manager/
├── cli/
│ ├── task.go
├── database/
│ ├── database.go
├── model/
│ ├── task.go
├── main.go
├── .env
└── go.mod
Step 2: Install Dependencies
Run the following commands in the root directory of the project.
$ go get github.com/urfave/cli/v2
$ go get github.com/joho/godotenv
$ go get go.mongodb.org/mongo-driver/v2/mongo
$ go get github.com/gookit/color
$ go get github.com/jedib0t/go-pretty/v6/table
github.com/urfave/cli/v2
: This is a popular library for building command-line applications in Go. It provides a simple and elegant way to define commands, flags, and arguments, making it easier to create user-friendly CLI tools.github.com/joho/godotenv
: This package is used for loading environment variables from a.env
file into the Go application.go.mongodb.org/mongo-driver/v2/mongo
: This is the official MongoDB driver for Go. It allows Go applications to interact with MongoDB databases, providing functionality for connecting to the database, performing CRUD operations, and executing queries.github.com/gookit/color
: This library provides utilities for coloring and styling text output in the terminal. It can be used to enhance the visual appearance of command-line applications by adding colors, bold text, and other styles to the output.github.com/jedib0t/go-pretty/v6/table
: This package is used for creating and formatting tables in the terminal. It allows data to displayed in a structured and visually appealing way, making it easier to read and understand.
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, create a Task-Manager
database and tasks
collection in the cluster.
Next, in the .env
file, add the connection URL, database and collection names as an environment variables.
Step 4: Environmental Variables
In /.env
, we will set the following environmenntal variables:
MONGODB_URI= <mongodb_URI>
MONGODB_DB_NAME= <db_name> # i.e Task-Manager
MONGODB_COLLECTION= <collection> # i.e tasks
Step 5: Setting Up a MongoDB Connection
In database/database.go
, we will set up the MongoDB connection.
package database
import (
"log"
"os"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/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(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 (
"log"
"task-manager/database"
)
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.
// model/task.go
package model
import (
"context"
"errors"
"fmt"
"task-manager/database"
"time"
"github.com/gookit/color"
"github.com/jedib0t/go-pretty/v6/table"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// Task represents a task in the task manager
type Task struct {
ID bson.ObjectID `bson:"_id"`
Title string `bson:"title"`
Completed bool `bson:"completed"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
// ==========================
// Task Retrieval Functions
// ==========================
// GetAllTasks retrieves all tasks from the database based on the provided filter
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
}
// iterate over cursor
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
}
// once exhausted, close the cursor
cursor.Close(ctx)
if len(tasks) == 0 {
return tasks, mongo.ErrNoDocuments
}
return tasks, nil
}
// FilterTasks retrieves tasks based on their completion status
func FilterTasks(status bool) ([]bson.M, error) {
// filter completed tasks
filter := bson.D{{Key: "completed", Value: status}}
results, err := GetAllTasks(filter)
if err != nil {
return results, err
}
return results, err
}
// ==========================
// Task Manipulation Functions
// ==========================
// AddTask adds a new task to the database
func AddTask(task Task) error {
collection := database.GetDBCollection()
_, err := collection.InsertOne(context.TODO(), &task)
if err != nil {
return err
}
return nil
}
// CompleteTask marks a task as completed
func CompleteTask(taskID string) error {
objectID, err := bson.ObjectIDFromHex(taskID)
if err != nil {
return err
}
// get collection
collection := database.GetDBCollection()
filter := bson.D{{Key: "_id", Value: objectID}}
update := bson.D{{Key: "$set", Value: bson.D{{Key: "completed", Value: true}}}}
_, err = collection.UpdateOne(context.TODO(), filter, update)
return err
}
// DeleteTask removes a task from the database
func DeleteTask(taskID string) error {
objectID, err := bson.ObjectIDFromHex(taskID)
if err != nil {
return err
}
// get database collection
collection := database.GetDBCollection()
filter := bson.D{{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
}
// ==========================
// Task Display Functions
// ==========================
// PrintTasks displays the tasks in a formatted table
func PrintTasks(tasks []bson.M) {
t := table.NewWriter()
t.SetTitle("All Tasks")
// Append table header
t.AppendHeader(table.Row{"#ID", "Title", "Completed"})
for _, tasks := range tasks {
id := tasks["_id"].(bson.ObjectID).Hex()
title := tasks["title"]
completed := tasks["completed"]
// Colorize output based on completion status
if tasks["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)
}
// Append task row to the table
t.AppendRow(table.Row{id, title, completed})
}
// Render table
fmt.Println(t.Render())
}
Step 7: Create the CLI Interface
In cli/task.go
, we will define the CLI commands for managing task
// cli/task.go
package cli
import (
"fmt"
"log"
"os"
"task-manager/model"
"time"
"github.com/gookit/color"
"github.com/urfave/cli/v2"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// Global variable for the CLI application
var taskApp *cli.App
// TaskCLI initializes the CLI application with commands
func TaskCLI() {
taskApp = &cli.App{
Name: "task-manager",
Version: "v1.0",
Compiled: time.Now(),
Usage: "Task management tool",
UsageText: "contrive - task management tool",
Commands: []*cli.Command{
// Command to list tasks
{
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 && err != mongo.ErrNoDocuments {
log.Fatal(err)
}
if tasks == nil {
tasks = []bson.M{}
}
model.PrintTasks(tasks)
return nil
},
Subcommands: []*cli.Command{
{
// Sub-commannd to list completed tasks
Name: "completed",
Action: func(ctx *cli.Context) error {
tasks, err := model.FilterTasks(true)
if err != nil {
log.Fatal(err)
}
model.PrintTasks(tasks)
return nil
},
},
{
// Sub-command to list uncompleted tasks
Name: "uncompleted",
Action: func(ctx *cli.Context) error {
tasks, err := model.FilterTasks(false)
if err != nil {
log.Fatal(err)
}
model.PrintTasks(tasks)
return nil
},
},
},
},
// Commannd to add new tasks
{
Name: "add",
Aliases: []string{"a"},
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() >= 2 {
color.Red.Println("invalid parameters!")
return nil
}
// retrieve task from cli
title := ctx.Args().First()
task := model.Task{
ID: bson.NewObjectID(),
Title: title,
Completed: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// add new task
if err := model.AddTask(task); err != nil {
color.Red.Println("Error adding task", err)
}
color.Green.Println("Task added successfully!")
return nil
},
},
// Command to complete task
{
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!")
} else if ctx.Args().Len() >= 2 {
color.Red.Println("invalid parameters!")
return nil
}
id := ctx.Args().First()
if err := model.CompleteTask(id); err != nil {
color.Red.Println("Error completing task", err)
}
color.Green.Println("Task completed successfully")
return nil
},
},
// Command to delete task
{
Name: "delete",
Aliases: []string{"rm"},
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() == 0 {
color.Red.Println("Provide task id to complete task!")
} else if ctx.Args().Len() >= 2 {
color.Red.Println("invalid parameters!")
return nil
}
id := ctx.Args().First()
if err := model.DeleteTask(id); err != nil {
color.Red.Println("Error completing task", err)
}
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 (
"log"
"task-manager/cli"
"task-manager/database"
)
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 "Name of task"
- List 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.