Building a To-Do List With MongoDB and Golang
In this article, consider how you can write your small service in Go in a couple of hours and put everything in a database.
Join the DZone community and get the full member experience.
Join For FreeHi, there! 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.
Let's start our journey with Golang and MongoDB.
Why Golang?
I want to show the keys:
- Minimalistic design and fast compilation
- Strong concurrency model with Goroutines and channels
- Huge ecosystem
- Cross-platform from the box
Another factor is not to spend much time studying libraries or open-source solutions. In my case, I want to create a microservice that will work almost out of the box. The Golang programming language has everything for this.
I will note, however, that the language is already rich in very cool projects that solve many problems for the developer. Projects such as:
- Gin: High-performance HTTP web framework
- Viper: Configuration solution (JSON, properties, YML files) for Go applications
- GORM: ORM library
- Protocol Buffers (Protobuf): The best way to serialize structured data
We will not review them within the article's framework, but perhaps I will write something about them later. With Protobuf, I have already written an interesting article, "From JSON to FlatBuffers: Enhancing Performance in Data Serialization."
Installation
Installing Go Language
Visit golang.org and download. Then, go to the terminal and check it.
go version
IDE
Just install VS Code (it's free).
And then add the Golang extension:
Type your first code:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
And run it:
go run main.go
That's it! The next step is to choose the best option to collect our data.
Our Task Structure
I think our first structure should be really simple. Let's start with 3 fields:
- Title (text)
- Description (text)
- Status (bool)
JSON file as a reference:
{
"title": "Go to the groceries",
"description": "Purchase milk, eggs, and bread",
"completed": false
}
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.
- Flexible Schema
- Scalability: It supports horizontal scaling.
- Rich Query Language
For our small service, we can run it as docker-compose
.
# Use root/example as user/password credentials
version: '3.1'
services:
mongo:
image: mongo
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
I prefer Compass GUI for working with our data (collections, databases, etc.). Download it here.
Just run it and set your credentials to "Advanced options." It works perfectly and can help you find problems and optimize requests if you need them.
System Design (Really Small)
For our service, of course, the best option is to create CRUD (Create, Read, Update, Delete) methods, something like this (without delete):
http.HandleFunc("/api/v1/add", todoHandler.AddTask)
http.HandleFunc("/api/v1/get-all", todoHandler.GetAllTasks)
http.HandleFunc("/api/v1/update", todoHandler.UpdateTask)
http.HandleFunc("/api/v1/complete", todoHandler.CompleteTask)
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
The project structure might be like this:
todo-list/
│
├── cmd/
│ └── main.go
├── pkg/
│ └── handler
│ └── add_task.go
│ └── http_handler.go
│ └── complite_task.go
│ └── get_all_task.go
│ └── update_task.go
│ └── mapper
│ └── task.go
│ └── model
│ └── task.go
│ └── usecase
│ └── task
│ └── repository
│ └── add_task.go
│ └── complite_task.go
│ └── get_all_task.go
│ └── mongo_repositiry.go
│ └── repository.go
│ └── update_task.go
│ └── service
│ └── add_task.go
│ └── complite_task.go
│ └── get_all_task.go
│ └── service.go
│ └── update_task.go
└── go.mod
As we can see, we have a "go.mod" file, but what is it? It is a packer manager or dependency manager. We can install and add external libs and use them as well. For our example, we need a couple of commands using "go mod".
- Init our app ->
go mod init todo-service
. We will init everything; the dependency manager will create files and add everything we need. - Add extra dependency using
go mod add "link"
.
You can read more on the Go Modules Reference page.
Then, let's focus on only one method — adding tasks. For further exploration and complete code examples, visit the GitHub repository Golang Workshop.
Connection to MongoDB
Using only two dependencies, we can create a connection:
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
Of course, we need to add it to our service. Please use the commands:
go get "go.mongodb.org/mongo-driver/mongo"
and
go get "go.mongodb.org/mongo-driver/mongo/options"
Then, write a piece of code:
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!")
}
Data structures 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.
1. Repository layer:
type Repository interface {
AddTask(ctx context.Context, task model.MongoTask) error
}
func (r *MongoRepository) AddTask(ctx context.Context, task model.MongoTask) error {
task.ID = primitive.NewObjectID()
_, err := r.collection.InsertOne(ctx, task)
}
2. Service layer:
type TodoService interface {
AddTask(ctx context.Context, task model.Task) error
}
func (s *Service) AddTask(ctx context.Context, task model.Task) error {
return s.Repo.AddTask(ctx, mapper.MapToDto(task))
}
3. Handler layer (to process HTTP requests):
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)
}
Now, we need to install the database connection and initialize the "layers" dependency in the main.go
file.
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: run service and shutdown
}
And test it with the request:
# 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..."
}
That's all. Then, we must add and implement new methods, and our service will be ready to work.
Conclusion
We've created a small, yet robust task management service using Golang and MongoDB.
As we can see, if we need to build a small service, we do it really fast without many obstacles. In my case, I would really like to use MongoDB as the main database if I have documents. It's just easy to manage.
It can also be noted that it would be no worse in other programming languages. In some places, it's even faster. For example, if you use Python and FastAPI - and here I may disagree with you. The Golang language is still based on the paradigms of programming languages like C++ and Java, where there is OOP. Such code and methodology allow you to keep the code as clear and clean as possible.
For a start, it will be good to consolidate such factors as a base, which will help you understand TDD and other methodologies. I will also note that to avoid overloading the article with metrics and text, I omitted comparing performance with other languages. I will note that Golang has no problems with this, and having written the main function, you already run it in your thread. You can easily find comparisons on the Internet, including benchmarks.
For further exploration and complete code examples, visit the GitHub repository (linked earlier).
Thanks and take care!
Published at DZone with permission of Ilia Ivankin. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments