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.
The Rise of Cloud-Native Development and Microservices
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:
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.
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:
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
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
}
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.
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!