Working MongoDB with Golang
Tags: go, mongodb, programming, database
Published: 2021-11-11 14:23:47

Every tutorial has a story. In that tutorial you'll find out different contents that is related to MongoDB, GoLang and working with mock data and deployment. Here is my content.

## Project structure

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/si76vvkkrnej4p7fnrm7.png)

PS: Here is the one different folder that name is dummy_api. That folder has own main file. What does it mean? When I run the main.go file before I'll add mock data. If you didn't catch up [Working with the marshal and unmarshal](https://dev.to/bseyhan/hey-marshal-1134) tutorial you should read it. Another important topics is "context". [Essential Go](https://www.programming-books.io/essential/go/context.todo-vs.context.background-d5224e27ff724a33a79cb4e03a5eb333.html)

> context.TODO()
**TODO returns a non-nil, empty Context.** Code should use context.TODO when it’s unclear which Context to use or it is not yet available (because the surrounding function has not yet been extended to accept a Context parameter). TODO is recognized by static analysis tools that determine whether Contexts are propagated correctly in a program.

> context.Background()
Background returns a non-nil, empty Context. **It is never canceled**, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7vxyfskn1d4ubz6xvah.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4rj2xahd9pmue1bw7hwa.png)
 
Another different package is about MongoDb Client however I'll talk about below.

```
package main

import (
	"context"
	"encoding/json"
	"io/ioutil"
	"os"

	"github.com/bburaksseyhan/contact-api/src/pkg/client/mongodb"
	"github.com/bburaksseyhan/contact-api/src/pkg/model"
	"github.com/sirupsen/logrus"
)

func main() {

	contactsJson, err := os.Open("contacts.json")

	if err != nil {
		logrus.Error("contact.json an error occurred", err)
	}

	defer contactsJson.Close()

	var contacts []model.Contact

	byteValue, _ := ioutil.ReadAll(contactsJson)

	//unmarshall data
	if err := json.Unmarshal(byteValue, &contacts); err != nil {
		logrus.Error("unmarshall an error occurred", err)
	}

	logrus.Info("Data\n", len(contacts))

	//import mongo client
	client := mongodb.ConnectMongoDb("mongodb://localhost:27017")
	logrus.Info(client)

	defer client.Disconnect(context.TODO())

	collection := client.Database("ContactDb").Collection("contacts")

	logrus.Warn("Total data count:", &contacts)

	for _, item := range contacts {
		collection.InsertOne(context.TODO(), item)
	}

	logrus.Info("Data import finished...")
}
```
Firstly let's open the terminal and goes to dummy_api directory. Another important thing, is database running? Let's have look. 

`docker compose up`
 
`docker container ls`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d16ayf3uyojwwayoijzd.png)
 
## Working with mock data

I was creating mock data from [Mockaroo](https://www.mockaroo.com/)

## Working with MongoDB queries

```
docker exec -it ad2d44477f28 mongo //connect to mongodb cli

help

show dbs // return all database names

use ContactDb 

show collections // return collection name

db.contacts.find() //return all collections

db.contacts.find({}).count() // return row count

db.contacts.find({}).pretty({}) // return rows with a format

db.contacts.find({"email":""})
db.dropDatabase() // remove database
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hy6w8lu2ongg1yklzhzo.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymr8d14tf030y2nfgz7x.png)
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78uwjsnba5st7xk4bd9s.png)
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm5rjo9g3puwdcszitjz.png)
 
## Using packages

* "go get -u go.mongodb.org/mongo-driver/bson"
* "go get -u go.mongodb.org/mongo-driver/mongo"
* "go get -u go.mongodb.org/mongo-driver/mongo/options"
* "go get -u github.com/gin-gonic/gin"
* "go get -u github.com/sirupsen/logrus"
* "go get -u github.com/spf13/viper"

## Implementation

### main.go file

That file read config.yml or .env file after that call the Init function.

```
package main

import (
	"os"

	"github.com/bburaksseyhan/contact-api/src/cmd/utils"
	"github.com/bburaksseyhan/contact-api/src/pkg/server"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

func main() {

	config := read()
	log.Info("Config.yml", config.Database.Url)

	mongoUri := os.Getenv("MONGODB_URL")

	if mongoUri != "" {
		config.Database.Url = mongoUri
	}

	log.Info("MONGODB_URL", mongoUri)

	server.Init(config.Database.Url)
}

func read() utils.Configuration {
	//Set the file name of the configurations file
	viper.SetConfigName("config")

	// Set the path to look for the configurations file
	viper.AddConfigPath(".")

	// Enable VIPER to read Environment Variables
	viper.AutomaticEnv()

	viper.SetConfigType("yml")
	var config utils.Configuration

	if err := viper.ReadInConfig(); err != nil {
		log.Error("Error reading config file, %s", err)
	}

	err := viper.Unmarshal(&config)
	if err != nil {
		log.Error("Unable to decode into struct, %v", err)
	}

	return config
}

```

ConnectMongoDb function takes the MongoDB URL parameter so this function opens the connection and check the status. [Related Documentation](https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial)

```
package mongodb

import (
	"context"

	log "github.com/sirupsen/logrus"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

func ConnectMongoDb(url string) *mongo.Client {

	clientOptions := options.Client().ApplyURI(url)

	// Connect to MongoDB
	client, err := mongo.Connect(context.TODO(), clientOptions)

	if err != nil {
		log.Fatal(err)
	}

	// Check the connection
	err = client.Ping(context.TODO(), nil)

	if err != nil {
		log.Fatal(err)
	}

	log.Info("MongoClient connected")

	return client
}


```

unfinished contact_handler.go file. HealthCheck function is not only health check. That function checks the MongoDb database status with a timeout. If any cancellation comes from the server, context will be triggered and response will be un-health. Let's think the opposite result will be pong. 

```
package handler

import (
	"context"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"go.mongodb.org/mongo-driver/mongo"
)

type ContactHandler interface {
	GetAllContacts(*gin.Context)
	GetContactByCity(*gin.Context)
	HealthCheck(*gin.Context)
}

type contactHandler struct {
	client *mongo.Client
}

func NewContactHandler(client *mongo.Client) ContactHandler {
	return &contactHandler{client: client}
}

func (ch *contactHandler) GetAllContacts(c *gin.Context) {
	_, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	//request on repository

	c.JSON(http.StatusOK, gin.H{"contacts": "pong"})
}

func (ch *contactHandler) GetContactByCity(c *gin.Context) {
	_, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	//request on repository

	c.JSON(http.StatusOK, gin.H{"contacts": "pong"})
}

func (ch *contactHandler) HealthCheck(c *gin.Context) {

	ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	if ctxErr != nil {
		logrus.Error("somethig wrong!!!", ctxErr)
	}

	if err := ch.client.Ping(ctx, nil); err != nil {
		c.JSON(http.StatusOK, gin.H{"status": "unhealty"})
	}

	c.JSON(http.StatusOK, gin.H{"status": "pong"})
}

```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhfryzgpyghgx4hiwng4.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tqtsmwnvkyp53mjxs5cl.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xv7rdnfwtfqxrm0kg9kz.png)
   
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58j0jyc7dnz6bomhcblw.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8w0jdfb2lnwpmins1kil.png)

# Completed codes

## main
```
package main

import (
	"os"

	"github.com/bburaksseyhan/contact-api/src/cmd/utils"
	"github.com/bburaksseyhan/contact-api/src/pkg/server"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

func main() {

	config := read()
	log.Info("Config.yml", config.Database.Url)

	mongoUri := os.Getenv("MONGODB_URL")
	serverPort := os.Getenv("SERVER_PORT")
	dbName := os.Getenv("DBNAME")
	collection := os.Getenv("COLLECTION")

	if mongoUri != "" {
		config.Database.Url = mongoUri
		config.Server.Port = serverPort
		config.Database.DbName = dbName
		config.Database.Collection = collection
	}

	log.Info("MONGODB_URL", mongoUri)

	server.Init(config)
}

func read() utils.Configuration {
	//Set the file name of the configurations file
	viper.SetConfigName("config")

	// Set the path to look for the configurations file
	viper.AddConfigPath(".")

	// Enable VIPER to read Environment Variables
	viper.AutomaticEnv()

	viper.SetConfigType("yml")
	var config utils.Configuration

	if err := viper.ReadInConfig(); err != nil {
		log.Error("Error reading config file, %s", err)
	}

	err := viper.Unmarshal(&config)
	if err != nil {
		log.Error("Unable to decode into struct, %v", err)
	}

	return config
}

```

## config

```
package utils

type Configuration struct {
	Database DatabaseSetting
	Server   ServerSettings
}

type DatabaseSetting struct {
	Url        string
	DbName     string
	Collection string
}

type ServerSettings struct {
	Port string
}

```

## server

```
package server

import (
	"github.com/bburaksseyhan/contact-api/src/cmd/utils"
	"github.com/bburaksseyhan/contact-api/src/pkg/client/mongodb"
	"github.com/bburaksseyhan/contact-api/src/pkg/handler"
	repository "github.com/bburaksseyhan/contact-api/src/pkg/repository/mongodb"

	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func Init(config utils.Configuration) {

	// Creates a gin router with default middleware:
	// logger and recovery (crash-free) middleware
	router := gin.Default()

	client := mongodb.ConnectMongoDb(config.Database.Url)

	repo := repository.NewContactRepository(&config, client)
	handler := handler.NewContactHandler(client, repo)

	router.GET("/", handler.GetAllContacts)
	router.GET("/contacts/:email", handler.GetContactByEmail)
	router.POST("/contact/delete/:id", handler.DeleteContact)

	router.GET("/health", handler.HealthCheck)

	log.Info("port is :8080\n", config.Database.Url)

	// PORT environment variable was defined.
	router.Run(":" + config.Server.Port + "")
}

```

## handler
```
package handler

import (
	"context"
	"net/http"
	"strconv"
	"time"

	"github.com/bburaksseyhan/contact-api/src/pkg/model"
	db "github.com/bburaksseyhan/contact-api/src/pkg/repository/mongodb"

	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"go.mongodb.org/mongo-driver/mongo"
)

type ContactHandler interface {
	GetAllContacts(*gin.Context)
	GetContactByEmail(*gin.Context)
	DeleteContact(*gin.Context)

	HealthCheck(*gin.Context)
}

type contactHandler struct {
	client     *mongo.Client
	repository db.ContactRepository
}

func NewContactHandler(client *mongo.Client, repo db.ContactRepository) ContactHandler {
	return &contactHandler{client: client, repository: repo}
}

func (ch *contactHandler) GetAllContacts(c *gin.Context) {

	ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	var contactList []*model.Contact

	//request on repository
	if result, err := ch.repository.Get(ctx); err != nil {
		logrus.Error(err)
	} else {
		contactList = result
	}

	c.JSON(http.StatusOK, gin.H{"contacts": &contactList})
}

func (ch *contactHandler) GetContactByEmail(c *gin.Context) {

	ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	var contactList *model.Contact

	//get parameter
	email := c.Param("email")

	//request on repository
	if result, err := ch.repository.GetContactByEmail(email, ctx); err != nil {
		logrus.Error(err)
	} else {
		contactList = result
	}

	c.JSON(http.StatusOK, gin.H{"contacts": contactList})
}

func (ch *contactHandler) HealthCheck(c *gin.Context) {

	ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	if ctxErr != nil {
		logrus.Error("somethig wrong!!!", ctxErr)
	}

	if err := ch.client.Ping(ctx, nil); err != nil {
		c.JSON(http.StatusOK, gin.H{"status": "unhealty"})
	}

	c.JSON(http.StatusOK, gin.H{"status": "pong"})
}

func (ch *contactHandler) DeleteContact(c *gin.Context) {

	ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer ctxErr()

	//get parameter
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		logrus.Error("Can not convert to id", err)
	}

	//request on repository
	result, err := ch.repository.Delete(id, ctx)
	if err != nil {
		logrus.Error(err)
	}

	c.JSON(http.StatusOK, gin.H{"deleteResult.DeletedCount": result})
}
```

## repository
```
package repository

import (
	"context"

	"github.com/bburaksseyhan/contact-api/src/cmd/utils"
	"github.com/bburaksseyhan/contact-api/src/pkg/model"
	"github.com/sirupsen/logrus"
	log "github.com/sirupsen/logrus"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type ContactRepository interface {
	Get(ctx context.Context) ([]*model.Contact, error)
	GetContactByEmail(email string, ctx context.Context) (*model.Contact, error)
	Delete(id int, ctx context.Context) (int64, error)
}

type contactRepository struct {
	client *mongo.Client
	config *utils.Configuration
}

func NewContactRepository(config *utils.Configuration, client *mongo.Client) ContactRepository {
	return &contactRepository{config: config, client: client}
}

func (c *contactRepository) Get(ctx context.Context) ([]*model.Contact, error) {

	findOptions := options.Find()
	findOptions.SetLimit(100)

	var contacts []*model.Contact

	collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

	// Passing bson.D{{}} as the filter matches all documents in the collection
	cur, err := collection.Find(ctx, bson.D{{}}, findOptions)
	if err != nil {
		log.Fatal(err)
		return nil, err
	}

	// Finding multiple documents returns a cursor
	// Iterating through the cursor allows us to decode documents one at a time
	for cur.Next(context.TODO()) {
		// create a value into which the single document can be decoded
		var elem model.Contact
		if err := cur.Decode(&elem); err != nil {
			log.Fatal(err)
			return nil, err
		}

		contacts = append(contacts, &elem)
	}

	cur.Close(ctx)

	return contacts, nil
}

func (c *contactRepository) GetContactByEmail(email string, ctx context.Context) (*model.Contact, error) {

	findOptions := options.Find()
	findOptions.SetLimit(100)

	var contacts *model.Contact

	collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

	filter := bson.D{primitive.E{Key: "email", Value: email}}

	logrus.Info("Filter", filter)

	collection.FindOne(ctx, filter).Decode(&contacts)

	return contacts, nil
}

func (c *contactRepository) Delete(id int, ctx context.Context) (int64, error) {

	collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

	filter := bson.D{primitive.E{Key: "id", Value: id}}

	deleteResult, err := collection.DeleteOne(ctx, filter)
	if err != nil {
		log.Fatal(err)

		return 0, err
	}

	return deleteResult.DeletedCount, nil
}

```

## Deployment

`docker compose up`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhludq029z827nlr1z6f.png)
 

[Repository](https://github.com/bburaksseyhan/contact-api.git)

Thank you,