How to Build Your Own TODO-list Service With Golang and MongoDB

cover
23 Jul 2024

Many have wondered how a simple task sheet or applications that provide such functionality work. In this article, I invite you to consider how you can write your small service in Go in a couple of hours and put everything in a database.


Why Goalng?

The Keys are Simplicity and Efficiency.

  • Minimalistic Design: Easy to learn and use, focusing on simplicity.

  • Fast Compilation: Compiled to native code, ensuring speedy execution and deployment.

  • Concurrency Goroutines: Lightweight threads managed by Go runtime make concurrent programming more superficial and efficient. Channels: Facilitate safe communication between goroutines, avoiding the pitfalls of shared memory. Robust Standard Library.

  • Rich Ecosystem: Extensive standard library covering web servers, cryptography, I/O, and more.

  • Cross-Platform: Compiles to various operating systems and architectures without modification.

The Rise of Cloud-Native Development and Microservices

  • Docker allows developers to package applications into containers, ensuring consistency across different environments. For example, a web server can be run in a Docker container to ensure it behaves similarly in development and production. Consul Consul provides service discovery and configuration management.

  • K8s (Kubernetes) - automates the deployment, scaling, and management of containerized applications.

  • gRPC is a high-performance, open-source universal RPC framework. Example: Using gRPC to enable efficient communication between microservices written in different languages.

  • Terraform is used to safely and efficiently build, change, and version infrastructure. An example is using Terraform scripts to provision cloud infrastructure on AWS.

Golang and the Internet of Things (IoT)

  • NATS is a simple, high-performance, open-source messaging system for cloud-native applications, IoT messaging, and microservices architectures.

  • InfluxData is a platform for handling time series data, which is essential for IoT applications.

Growing Adoption of Machine Learning and Artificial Intelligence (AI)

  • “TensorFlow Go” provides machine learning tools that can be used with Go.

  • Gorgonia is a library that brings machine-learning capabilities to Go.

  • GoLearn is a machine-learning library for Go.

The Expanding Golang Ecosystem

  • Gin is a high-performance HTTP web framework written in Golang.

  • Viper is a complete configuration solution for Go applications. For example, It Manages application configuration using Viper to support formats like JSON, YAML, and TOML.

  • Cobra is a library that creates powerful modern CLI applications.

  • GORM is an ORM library for Golang.

  • Protocol Buffers (Protobuf) is a method developed by Google for serializing structured data.

Why MongoDB?

We need to collect data for our tasks and be flexible. We don't need to create a schema or relationship between something.

What can we have using it:

  • Flexible Schema: MongoDB allows for schema-less design, making it easy to handle unstructured or semi-structured data.

  • Scalability: It supports horizontal scaling, allowing you to distribute data across multiple servers.

  • Rich Query Language: MongoDB provides a powerful query language, including support for complex queries, indexing, and aggregation.

    That’s nice for our example:

    {
    "_id": "66532b210d9944a92a88ef4b",
    "title": "Go to the groceries",
    "description": "Purchase milk, eggs, and bread",
    "completed": false
    }
    

A local run with docker:

version: '3.1'

services:

mongo:
  image: mongo
  ports:
    - "27017:27017"
  environment:
    MONGO_INITDB_ROOT_USERNAME: root
    MONGO_INITDB_ROOT_PASSWORD: example

Now, we have DB, but we need to work with it as well.

Compass

MongoDB Compass is a graphical user interface (GUI) for MongoDB designed to facilitate developers, database administrators, and data analysts' interactions with their MongoDB databases. It provides a user-friendly visual representation of the data and powerful tools for querying, managing, and optimizing databases.

Download it here: https://www.mongodb.com/products/tools/compass.

Why MongoDB Compass is Easy to Use:

  • Graphical Interface: The graphical interface reduces the learning curve associated with using MongoDB, making it accessible to users with varying technical expertise.

  • Drag-and-Drop Functionality: Many features, such as query building and schema design, use drag-and-drop functionality, simplifying complex operations and making them more intuitive.

  • Real-Time Feedback: Compass provides real-time feedback as you interact with your data, allowing you to see the results of your queries and modifications immediately.

  • Comprehensive Documentation and Support: MongoDB Compass is backed by extensive documentation and a supportive community. Users can easily find tutorials, guides, and forums to help them navigate any challenges.


Fast Installations Before We Start

Install VS code (It's free).

Visit https://code.visualstudio.com/.

Installing Go Visit golang.org to download the installer for your operating system.

Follow the installation instructions provided on the website.

Verify the installation by opening a terminal/command prompt and typing:

go version

And then add the Golang extension:

package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}

And run it:

go run main.go

System Design (Small, But Still ;D)

The document should have:

Title
Description
Status

Our previous JSON file as a reference: JSON

{
"_id": "66532b210d9944a92a88ef4b",
"title": "Go to the groceries",
"description": "Purchase milk, eggs, and bread",
"completed": false
}

Next step: Create main methods such as CRUD.

Create -The Create operation involves adding new records to a database. This is the initial step in data management, where new data entries are inserted into the database.

Read - The Read operation retrieves data from the database. It allows users to fetch and display data without modifying it.

Update - The Update operation involves modifying existing records in the database. It changes the data within a record while maintaining its identity.

Delete—The Delete operation permanently removes records from a database. It is often accompanied by a confirmation step to prevent accidental deletions.

We are going to implement only the “CREATE” or add method because I’d like to share a good example. After that, you can implement others.

Project structure:

todo-list/
│
├── cmd/
│   └── main.go
├── pkg/
│   └── handler
│      └── add_task.go
│      └── http_handler.go
│   └── mapper
│       └── task.go
│   └── model
│        └── task.go
│   └── usecase
│        └── task
│           └── repository
│               └── add_task.go
│               └── mongo_repositiry.go
│               └── repository.go
│           └── service
│               └── add_task.go
│               └── service.go
└── go.mod

I want to use the way to separate all responsibilities by folders.

  • Handler - HTTP layer
  • Model - structures for data
  • Use cases - business layers with service and repository.

Let's start with a data structure for our app:

package model

import "go.mongodb.org/mongo-driver/bson/primitive"

type Task struct {
  ID         string json:"id"
  Title      string json:"title"
  Desciption string json:"description"
  Completed  bool   json:"completed"
}

type MongoTask struct {
  ID         primitive.ObjectID json:"id" bson:"_id"
  Title      string             json:"title"
  Desciption string             json:"description"
  Completed  bool               json:"completed"
}

Task - for HTTP request, MongoTask - for MongoDb layer. Using two structures is easy because sometimes we don't need to send additional data to our users. For example, we might have a secret field, like a username, which we must hide. Now that we know CRUD, let's code it!

Repository layer:

type Repository interface {
  AddTask(ctx context.Context, task model.MongoTask) error
}

Service layer:

 type TodoService interface { 
  AddTask(ctx context.Context, task model.Task) error  
}

Let's connect and inject dependencies:

// Initialize repository, service, and handler
todoRepo := repository.NewMongoRepository(client)
todoService := service.NewService(todoRepo)
todoHandler := handler.NewHandler(todoService)

Finally, context and connections:

func main() {
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  // Set MongoDB client options
  clientOptions := options.Client().ApplyURI("mongodb://localhost:27017").SetAuth(options.Credential{
	Username: "root",
	Password: "example",
  })

  client, err := mongo.Connect(ctx, clientOptions)
  if err != nil {
	log.Fatal(err)
  }

  err = client.Ping(ctx, nil)
  if err != nil {
	log.Fatal(err)
  }

  log.Println("Connected to MongoDB!")

  // Initialize repository, service, and handler
  todoRepo := repository.NewMongoRepository(client)
  todoService := service.NewService(todoRepo)
  todoHandler := handler.NewHandler(todoService)

  // Set up routes
  http.HandleFunc("/api/v1/add", todoHandler.AddTask)

  // Create a server
  srv := &http.Server{
	Addr:    ":8080",
	Handler: nil,
  }
  // .. todo
}

Demo

Now, we have everything, and we can start to analyze what happens when we call our service.

curl -X POST http://localhost:8080/add

#-H "Content-Type: application/json"
#-d '{
"id": 1,
"title": "Buy groceries",
"completed": false
#}'

POST http://localhost:8080/api/v1/add
Content-Type: application/json

{
  "title": "Add description to the structure",
  "description": "your desc here..."
}

We will process the request using the handler layer, decode it using JSON lib, and send the model to the service layer.

func (h *Handler) AddTask(w http.ResponseWriter, r *http.Request) {
  ctx := context.Background()
  var task model.Task
  err := json.NewDecoder(r.Body).Decode(&task)
  if err != nil {
	http.Error(w, "Invalid request body", http.StatusBadRequest)
	return
  }

  err = h.Service.AddTask(ctx, task)
  if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
  }

  w.WriteHeader(http.StatusCreated)
}

Next step, process it in the service layer: (just proxy and convert the model to DTO or Entity for MongoDb).

func (s *Service) AddTask(ctx context.Context, task model.Task) error {
  return s.Repo.AddTask(ctx, mapper.MapToDto(task))
}

Lastly, use the MongoDB client, and save the task to DB.

func (r *MongoRepository) AddTask(ctx context.Context, task model.MongoTask) error {
  task.ID = primitive.NewObjectID()

  _, err := r.collection.InsertOne(ctx, task) 

  return err
}

That's it! We finished the first method for saving the task. You can implement three more methods, or you can check them out here: Golang Workshop.


Conclusion

In conclusion, we've created a small yet robust task management service using Golang and MongoDB. This exercise demonstrated how Golang's simplicity, concurrency features, and MongoDB's flexibility and scalability provide a powerful platform for building modern web applications.

With the right tools and architecture, you can efficiently manage and manipulate data, creating scalable and maintainable services.

Now, we know how to build our to-do list and understand that it’s not hard to code.

Take care!