Skip to main content

Gin MongoDB Integration

Introduction

Integrating MongoDB with the Gin web framework allows you to build powerful web applications with a flexible NoSQL database backend. MongoDB is a document-oriented database that stores data in JSON-like documents, making it a great choice for applications with evolving data requirements. In this guide, we'll learn how to connect MongoDB with Gin, create CRUD operations, and implement common database patterns.

Prerequisites

Before we begin, ensure you have the following installed:

  • Go programming language (1.16+ recommended)
  • Gin web framework
  • MongoDB (local installation or cloud service)
  • The official MongoDB Go driver

Let's start by installing the necessary Go packages:

bash
go get -u github.com/gin-gonic/gin
go get go.mongodb.org/mongo-driver/mongo

Setting Up MongoDB Connection

First, let's create a database connection utility that we can reuse across our application.

Creating a Database Connection

go
package database

import (
"context"
"log"
"time"

"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

// DBClient is the MongoDB client
var DBClient *mongo.Client

// Connect establishes a connection to MongoDB
func Connect(uri string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

clientOptions := options.Client().ApplyURI(uri)
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
return err
}

// Check the connection
err = client.Ping(ctx, nil)
if err != nil {
return err
}

DBClient = client
log.Println("Connected to MongoDB!")
return nil
}

// GetCollection returns a handle to a MongoDB collection
func GetCollection(dbName string, collName string) *mongo.Collection {
return DBClient.Database(dbName).Collection(collName)
}

// Close disconnects from MongoDB
func Close() {
if DBClient == nil {
return
}

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

if err := DBClient.Disconnect(ctx); err != nil {
log.Fatal(err)
}
log.Println("Connection to MongoDB closed.")
}

Defining Data Models

Let's create a simple data model for our application. In this example, we'll build a basic book management system.

go
package models

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

// Book represents a book in our library
type Book struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title string `json:"title" bson:"title"`
Author string `json:"author" bson:"author"`
ISBN string `json:"isbn" bson:"isbn"`
Published int `json:"published" bson:"published"`
Pages int `json:"pages" bson:"pages"`
Genre string `json:"genre" bson:"genre"`
}

Implementing CRUD Operations

Now, let's implement the CRUD (Create, Read, Update, Delete) operations for our books collection.

Creating a Book Repository

go
package repository

import (
"context"
"time"

"your-app/database"
"your-app/models"

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

type BookRepository struct {
collection *mongo.Collection
}

func NewBookRepository(dbName string, collName string) *BookRepository {
return &BookRepository{
collection: database.GetCollection(dbName, collName),
}
}

// CreateBook adds a new book to the database
func (r *BookRepository) CreateBook(book models.Book) (primitive.ObjectID, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := r.collection.InsertOne(ctx, book)
if err != nil {
return primitive.NilObjectID, err
}

return result.InsertedID.(primitive.ObjectID), nil
}

// GetBooks retrieves all books from the database
func (r *BookRepository) GetBooks() ([]models.Book, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var books []models.Book
cursor, err := r.collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

if err = cursor.All(ctx, &books); err != nil {
return nil, err
}

return books, nil
}

// GetBookByID retrieves a book by its ID
func (r *BookRepository) GetBookByID(id string) (models.Book, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var book models.Book
objID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return book, err
}

err = r.collection.FindOne(ctx, bson.M{"_id": objID}).Decode(&book)
return book, err
}

// UpdateBook updates an existing book
func (r *BookRepository) UpdateBook(id string, book models.Book) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

objID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return err
}

update := bson.M{
"$set": bson.M{
"title": book.Title,
"author": book.Author,
"isbn": book.ISBN,
"published": book.Published,
"pages": book.Pages,
"genre": book.Genre,
},
}

_, err = r.collection.UpdateOne(ctx, bson.M{"_id": objID}, update)
return err
}

// DeleteBook removes a book from the database
func (r *BookRepository) DeleteBook(id string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

objID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return err
}

_, err = r.collection.DeleteOne(ctx, bson.M{"_id": objID})
return err
}

Creating Gin API Handlers

Now let's create the Gin handlers that will interact with our MongoDB repository:

go
package handlers

import (
"net/http"

"your-app/models"
"your-app/repository"

"github.com/gin-gonic/gin"
)

type BookHandler struct {
repo *repository.BookRepository
}

func NewBookHandler(repo *repository.BookRepository) *BookHandler {
return &BookHandler{
repo: repo,
}
}

// CreateBook handles the creation of a new book
func (h *BookHandler) CreateBook(c *gin.Context) {
var book models.Book
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

id, err := h.repo.CreateBook(book)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusCreated, gin.H{"message": "Book created successfully", "id": id.Hex()})
}

// GetBooks returns all books
func (h *BookHandler) GetBooks(c *gin.Context) {
books, err := h.repo.GetBooks()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, books)
}

// GetBookByID returns a specific book
func (h *BookHandler) GetBookByID(c *gin.Context) {
id := c.Param("id")
book, err := h.repo.GetBookByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Book not found"})
return
}

c.JSON(http.StatusOK, book)
}

// UpdateBook updates a book
func (h *BookHandler) UpdateBook(c *gin.Context) {
id := c.Param("id")
var book models.Book
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if err := h.repo.UpdateBook(id, book); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Book updated successfully"})
}

// DeleteBook removes a book
func (h *BookHandler) DeleteBook(c *gin.Context) {
id := c.Param("id")
if err := h.repo.DeleteBook(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Book deleted successfully"})
}

Setting Up the Gin Router

Now, let's set up our Gin router to handle our book API endpoints:

go
package main

import (
"log"

"your-app/database"
"your-app/handlers"
"your-app/repository"

"github.com/gin-gonic/gin"
)

func main() {
// Connect to MongoDB
mongoURI := "mongodb://localhost:27017"
if err := database.Connect(mongoURI); err != nil {
log.Fatal(err)
}
defer database.Close()

// Setup repositories and handlers
bookRepo := repository.NewBookRepository("library", "books")
bookHandler := handlers.NewBookHandler(bookRepo)

// Initialize Gin router
router := gin.Default()

// Book routes
bookRoutes := router.Group("/api/books")
{
bookRoutes.GET("/", bookHandler.GetBooks)
bookRoutes.GET("/:id", bookHandler.GetBookByID)
bookRoutes.POST("/", bookHandler.CreateBook)
bookRoutes.PUT("/:id", bookHandler.UpdateBook)
bookRoutes.DELETE("/:id", bookHandler.DeleteBook)
}

// Start the server
if err := router.Run(":8080"); err != nil {
log.Fatal(err)
}
}

Testing the API

Let's test our API using some HTTP requests. You can use tools like curl, Postman, or a REST client in your code editor.

Add a New Book

Request:

bash
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{
"title": "The Go Programming Language",
"author": "Alan A. A. Donovan and Brian W. Kernighan",
"isbn": "978-0134190440",
"published": 2015,
"pages": 400,
"genre": "Programming"
}'

Response:

json
{
"message": "Book created successfully",
"id": "6078a4f9b54e2a18e8b4a1f2"
}

Get All Books

Request:

bash
curl http://localhost:8080/api/books

Response:

json
[
{
"id": "6078a4f9b54e2a18e8b4a1f2",
"title": "The Go Programming Language",
"author": "Alan A. A. Donovan and Brian W. Kernighan",
"isbn": "978-0134190440",
"published": 2015,
"pages": 400,
"genre": "Programming"
}
]

Advanced MongoDB Features with Gin

Implementing Pagination

Pagination is essential when dealing with large datasets. Let's add pagination to our book listing endpoint:

go
// GetBooks retrieves books with pagination
func (r *BookRepository) GetBooks(page, limit int) ([]models.Book, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var books []models.Book

// Calculate skip
skip := (page - 1) * limit

// Get total count
totalCount, err := r.collection.CountDocuments(ctx, bson.M{})
if err != nil {
return nil, 0, err
}

// Find with pagination
options := options.Find()
options.SetSkip(int64(skip))
options.SetLimit(int64(limit))

cursor, err := r.collection.Find(ctx, bson.M{}, options)
if err != nil {
return nil, 0, err
}
defer cursor.Close(ctx)

if err = cursor.All(ctx, &books); err != nil {
return nil, 0, err
}

return books, int(totalCount), nil
}

Update the handler to support pagination:

go
// GetBooks returns books with pagination
func (h *BookHandler) GetBooks(c *gin.Context) {
page := 1
limit := 10

// Parse pagination parameters
if pageParam := c.DefaultQuery("page", "1"); pageParam != "" {
if val, err := strconv.Atoi(pageParam); err == nil && val > 0 {
page = val
}
}

if limitParam := c.DefaultQuery("limit", "10"); limitParam != "" {
if val, err := strconv.Atoi(limitParam); err == nil && val > 0 && val <= 100 {
limit = val
}
}

books, total, err := h.repo.GetBooks(page, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{
"data": books,
"pagination": gin.H{
"current_page": page,
"limit": limit,
"total": total,
"total_pages": int(math.Ceil(float64(total) / float64(limit))),
},
})
}

Implementing Search Functionality

Let's add a search feature to find books by title or author:

go
// SearchBooks searches books by title or author
func (r *BookRepository) SearchBooks(query string) ([]models.Book, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var books []models.Book

// Create a search filter
filter := bson.M{
"$or": []bson.M{
{"title": bson.M{"$regex": query, "$options": "i"}},
{"author": bson.M{"$regex": query, "$options": "i"}},
},
}

cursor, err := r.collection.Find(ctx, filter)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

if err = cursor.All(ctx, &books); err != nil {
return nil, err
}

return books, nil
}

Add a handler for searching:

go
// SearchBooks handles search functionality
func (h *BookHandler) SearchBooks(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Search query is required"})
return
}

books, err := h.repo.SearchBooks(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, books)
}

Add the route to your router:

go
bookRoutes.GET("/search", bookHandler.SearchBooks)

Best Practices for MongoDB with Gin

  1. Use Context Management: Always use context with timeouts to prevent long-running operations.

  2. Error Handling: Implement proper error handling to provide meaningful responses.

  3. Connection Pooling: MongoDB Go driver includes connection pooling by default. Don't create new connections for every request.

  4. Validation: Implement validation for input data before sending it to MongoDB.

  5. Indexing: Create appropriate indexes on your MongoDB collections to optimize query performance:

go
// CreateIndexes sets up necessary indexes for the books collection
func (r *BookRepository) CreateIndexes() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Create an index on title
titleIndex := mongo.IndexModel{
Keys: bson.D{{Key: "title", Value: 1}},
Options: options.Index().SetBackground(true),
}

// Create an index on author
authorIndex := mongo.IndexModel{
Keys: bson.D{{Key: "author", Value: 1}},
Options: options.Index().SetBackground(true),
}

// Create a unique index on ISBN
isbnIndex := mongo.IndexModel{
Keys: bson.D{{Key: "isbn", Value: 1}},
Options: options.Index().SetUnique(true).SetBackground(true),
}

_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
titleIndex, authorIndex, isbnIndex,
})

return err
}

Real-World Application: A Complete Book API

Let's see how our Book API can be organized in a real-world application structure:

project/
├── config/
│ └── config.go # Application configuration
├── database/
│ └── mongodb.go # MongoDB connection handling
├── models/
│ └── book.go # Data models
├── repository/
│ └── book_repository.go # Database operations
├── handlers/
│ └── book_handler.go # API endpoint handlers
├── middlewares/
│ ├── auth.go # Authentication middleware
│ └── logging.go # Logging middleware
├── routes/
│ └── routes.go # Route definitions
├── utils/
│ └── response.go # Response utility functions
├── main.go # Application entry point
└── go.mod # Go module file

With this structure, we can implement additional features like:

  • Authentication and authorization
  • Input validation
  • Rate limiting
  • Logging and monitoring
  • Error handling middleware

Summary

In this guide, we've covered how to integrate MongoDB with the Gin web framework to build a robust RESTful API. We've explored:

  1. Setting up a MongoDB connection with the Go driver
  2. Creating data models and repositories
  3. Implementing CRUD operations
  4. Building Gin handlers and routes
  5. Adding advanced features like pagination and search
  6. Best practices for MongoDB and Gin integration

MongoDB's flexibility, combined with Gin's performance, makes for a powerful combination when building modern web applications. This NoSQL approach is particularly valuable for applications with evolving schema requirements or those dealing with large volumes of unstructured data.

Additional Resources

Exercises

  1. Add filtering capabilities to the book API (e.g., filter by genre, published year)
  2. Implement sorting options for the book listing endpoint
  3. Create a user authentication system using MongoDB and JWT
  4. Add a reviews system where users can leave reviews for books
  5. Implement data validation using a library like go-playground/validator


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)