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.

The Rise of Cloud-Native Development and Microservices

Golang and the Internet of Things (IoT)

Growing Adoption of Machine Learning and Artificial Intelligence (AI)

The Expanding Golang Ecosystem


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:

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:


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.

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!