DEV Community

Cover image for A CRUD API with Go, using the Gin framework and MongoDB.
Negin.S
Negin.S

Posted on

2

A CRUD API with Go, using the Gin framework and MongoDB.

Hi, here we want to create a simple and clean RESTful API built with Golang (Gin), MongoDB, and JWT Authentication.
This API allows you to register, log in, and perform user management operations securely.
you can find the source code here : sourcecode

Image description

πŸ“¦ Features of Project

  • πŸ”‘ User registration and login with secure password hashing
  • πŸ›‘οΈ JWT-based authentication
  • πŸ”„ Full ‍‍CRUD operations for users
  • βœ… Input validation using go-playground/validator
  • πŸ“š Swagger documentation with swaggo/gin-swagger
  • πŸ’Ύ MongoDB integration using mongo-driver
  • πŸ§ͺ Tests for core functionalities

βš™οΈ Prerequisites
β€ŒBefor starting make sure that you have these :

  • latest version of go
  • postman or curl for api testing

πŸ”¨ Initializing a Go project:

First of all create a folder for project :

mkdir go-crud-api
cd go-crud-api
Enter fullscreen mode Exit fullscreen mode

For initializing a Go project we usually run this command : go mod init . this command create a file (go.mod) in your directory , this file is using for dependency management of your project. so open your terminal & run this command go mod init then write this go mod init github.com/yourusername/go-crud-api in the file if you doesn't create any repo for your project you can just write go mod init my-project then change it to your repo address.

  • we use Gin for creating api :go get -u github.com/gin-gonic/gin

πŸ“ Project structure

go-crud-api/
β”‚
β”œβ”€β”€ main.go
β”œβ”€β”€ config/          ← Database settings and ...
β”‚   └── db.go
β”œβ”€β”€ controller/      ← Controllers (logic related to APIs)
β”‚   └── user.go
β”œβ”€β”€ model/           ← Data structures and models
β”‚   └── user.go
β”œβ”€β”€ middleware/      ← Middlewares (like auth)
β”‚   └── authMiddleware.go
β”œβ”€β”€ routes/          ← Routing
β”‚   └── userRoutes.go
β”œβ”€β”€ utils/           ← Helper functions (like ValidateToken)
β”‚   └── jwt.go
β”œβ”€β”€ go.mod / go.sum  ← Package information
└── README.md        ← Project description 
Enter fullscreen mode Exit fullscreen mode

🧠 Explain Code

1. go-crud-api/main.go

package main

import (
    "crud-api-go/config"
    "crud-api-go/routes"
    "github.com/gin-gonic/gin"
)

func main() {
    config.ConnectDB()
    router := gin.Default()
    routes.UserRoutes(router)
    router.Run("localhost:8080")
}
Enter fullscreen mode Exit fullscreen mode
  • The main function is the entry point of our program.

  • config.ConnectDB() : this line calls a function(ConnectDB)from config package. this function is responsible for establishing connection to your database.

  • router := gin.Default() : A router is a core of a web app , it determines how the app responds to different client request at different URL.

  • routes.UserRoutes(router) : This line calls a function userRoutes from the routes package,passing the Gin router as an argument.this function is responsible for defining all the API routes related to users (like creating, reading, updating, and deleting users).

  • router.Run("localhost:8080") : This line starts the Gin web server makes it listen for incoming HTTP requests on the address localhost:8080.

2. go-crud-api/config/db.go

Setting Up Our MongoDB Connection in Go**

Packages We Rely On:

  • time, context: Used for managing timeouts during the database connection process.
  • os, log, fmt: Essential for interacting with the operating system (reading environment variables), logging errors, and printing informational messages.
  • github.com/joho/godotenv: A library for loading environment variables from a .env file.
  • go.mongodb.org/mongo-driver/mongo & go.mongodb.org/mongo-driver/mongo/options: The official Go driver for MongoDB.

  • var DB *mongo.Database : Here, we declare a global variable DB of type *mongo.Database. This allows us to easily access the established database connection from anywhere in our Go application.

  • err := godotenv.Load() : We use the godotenv.Load() function to read key-value pairs from our .env file and make them available as environment variables within our application.

if err != nil {
    log.Fatal("❌ Error loading .env file")
}
Enter fullscreen mode Exit fullscreen mode

If the .env file failed to load the program execution will be halted.

mongoURI := os.Getenv("MONGO_URI")
databaseName := os.Getenv("DB_NAME")
Enter fullscreen mode Exit fullscreen mode

with os.Getenv we get the environmental variable for connecting to database.

clientOptions := options.Client().ApplyURI(mongoURI) : This line is using for setup connection settings.

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

A context is created with a 10-second timeout to prevent the connection from hanging indefinitely. The defer cancel() ensures that the context's resources are released when the function exits.

client, err := mongo.Connect(ctx, clientOptions) : Here, we attempt to establish a connection to our MongoDB instance using the configured clientOptions and the context we created.

DB = client.Database(databaseName) : Finally we setup the database instance, so it's accessible through out the entire application.

package config

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var DB *mongo.Database

func ConnectDB() {
    // Load environment variables
    err := godotenv.Load()
    if err != nil {
        log.Fatal("❌ Error loading .env file")
    }

    // Get URI and database name from .env file
    mongoURI := os.Getenv("MONGO_URI")
    databaseName := os.Getenv("DB_NAME")

    if mongoURI == "" {
        log.Fatal("❌ MONGO_URI is not set in .env")
    }
    if databaseName == "" {
        log.Fatal("❌ DB_NAME is not set in .env")
    }

    // Set up connection options
    clientOptions := options.Client().ApplyURI(mongoURI)

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

    // Connect to MongoDB
    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
        log.Fatal("❌ MongoDB connection error:", err)
    }

    // Ping the database to test connection
    err = client.Ping(ctx, nil)
    if err != nil {
        log.Fatal("❌ MongoDB ping error:", err)
    }

    fmt.Println("βœ… Connected to MongoDB Atlas!")

    // Set the selected database
    DB = client.Database(databaseName)
}
Enter fullscreen mode Exit fullscreen mode

3. go-crud-api/routes/userRoutes.go

This file is related to defining HTTP routes.we define public routes such as signup & login then we created a group of routes that are connected to the authentication middleware and include CRUD operations on users.

package routes

import (
    "crud-api-go/controller"
    "crud-api-go/middleware"

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

func UserRoutes(router *gin.Engine) {
    router.POST("/signup", controller.Signup)
    router.POST("/login", controller.Login)

    protected := router.Group("/")
    protected.Use(middleware.AuthMiddleware())
    {
        protected.GET("/users", controller.GetUsers)
        protected.GET("/users/:id", controller.GetUserByID)
        protected.POST("/users", controller.CreateUser)
        protected.PUT("/users/:id", controller.UpdateUser)
        protected.DELETE("/users/:id",controller.DeleteUser)
    }
}

Enter fullscreen mode Exit fullscreen mode

we define a function named UserRoutes.this function will be responsible for defining all the API endpoints related to the users. this function take a pointer to a gin .Engine as an argument. the gin.Engin is the main instance of the Gin router.

  • protected := router.Group("/") : this line creates a new route group using the router.Group("/") method.The / shows that all routes defined within this group will have a common base path.

  • protected.Use(middleware.AuthMiddleware()) : This means that for any request made to the routes defined within this group, the AuthMiddleware() function will be executed first.

4. go-crud-api/model/user.go

Model structures in this file are designed for three purposes: storing data in MongoDB (User), securely responding to clients (UserResponse and AuthResponse), and receiving input from clients for registration, login, or editing.

package model

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

type User struct {
    ID       primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
    Name     string             `json:"name" bson:"name" validate:"required,min=2,max=50"`
    Email    string             `json:"email" bson:"email" validate:"required,email"`
    Password string             `json:"password,omitempty" bson:"password,omitempty" validate:"required,min=6"`
}

type UserResponse struct {
    ID    primitive.ObjectID `json:"id"`
    Name  string             `json:"name"`
    Email string             `json:"email"`
}

type AuthResponse struct {
    Token string       `json:"token"`
    User  UserResponse `json:"user"`
}

type SignupInput struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

type LoginInput struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

type CreateUserInput struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

type UpdateUserInput struct {
    Name  string `json:"name" validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
}
Enter fullscreen mode Exit fullscreen mode

5. go-crud-api/controller/user.go

In this file we have 5 main controller functions for CRUD operations on users, all written using Gin and connected to the MongoDB database. We utilize input validation, and all requests, except for signup and login, require JWT authentication.

  • First of all we import packages , these packages are for interacting with the MongoDB database, managing time, performing validation, and the Gin framework for handling HTTP requests, config for database access and model for defining data structures.

  • Helper Function and Validation

func getUserCollection() *mongo.Collection {
    return config.DB.Collection("users")
}
Enter fullscreen mode Exit fullscreen mode

This function retrieves the MongoDB collection named users from the existing DB connection in the config package.

var validate = validator.New()
Enter fullscreen mode Exit fullscreen mode

We use the 'validator' package to check user inputs (e.g., whether fields are required or the email structure is correct).

  • GetUsers : The GetUsers function retrieves a list of users from the database and responds in JSON format.

  • GetUserByID : Retrieving a specific user from the database by ID and responding in JSON format.

  • CreateUser : Creating a new user and saving it to MongoDB.

  • UpdateUser : Updating user information in the database based on the ID.

  • DeleteUser : Deleting a user from the database based on the ID.

package controller

import (
    "context"
    "crud-api-go/config"
    "crud-api-go/model"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)

func getUserCollection() *mongo.Collection {
    return config.DB.Collection("users")
}

var validate = validator.New()

func GetUsers(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    cursur, err := getUserCollection().Find(ctx, bson.M{})
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "error fetching users"})
        return
    }
    defer cursur.Close(ctx)

    var users []model.User
    if err := cursur.All(ctx, &users); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "error parsing users"})
        return
    }

    var userslist []model.UserResponse
    for _, u := range users {
        userslist = append(userslist, model.UserResponse{
            ID:    u.ID,
            Name:  u.Name,
            Email: u.Email,
        })
    }
    c.JSON(http.StatusOK, userslist)

}

func GetUserByID(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    idParam := c.Param("id")
    objectId, err := primitive.ObjectIDFromHex(idParam)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID Format"})
        return
    }

    var user model.User
    err = getUserCollection().FindOne(ctx, bson.M{"_id": objectId}).Decode(&user)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
        return
    }
    c.JSON(http.StatusOK, user)
}

func CreateUser(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var input model.CreateUserInput
    if err := c.BindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    if err := validate.Struct(input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    newUser := model.User{
        ID:       primitive.NewObjectID(),
        Name:     input.Name,
        Email:    input.Email,
        Password: input.Password, 
    }

    _, err := getUserCollection().InsertOne(ctx, newUser)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
        return
    }

    c.JSON(http.StatusCreated, model.UserResponse{
        ID:    newUser.ID,
        Name:  newUser.Name,
        Email: newUser.Email,
    })
}

func UpdateUser(c *gin.Context) {
    idParam := c.Param("id")

    objectId, err := primitive.ObjectIDFromHex(idParam)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
        return
    }

    var updatedUser model.UpdateUserInput
    if err := c.BindJSON(&updatedUser); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
        return
    }

    if err := validate.Struct(updatedUser); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

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

    update := bson.M{
        "$set": bson.M{
            "name":  updatedUser.Name,
            "email": updatedUser.Email,
        },
    }

    res, err := getUserCollection().UpdateOne(ctx, bson.M{"_id": objectId}, update)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
        return
    }

    if res.MatchedCount == 0 {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }

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

func DeleteUser(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    idParam := c.Param("id")
    objectId, err := primitive.ObjectIDFromHex(idParam)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
        return
    }

    res, err := getUserCollection().DeleteOne(ctx, bson.M{"_id": objectId})
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"})
        return
    }

    if res.DeletedCount == 0 {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
}
Enter fullscreen mode Exit fullscreen mode

6. go-crud-api/controller/auth.go

In this file, we have two functions: Signup and Login. Signup registers a new user, hashes their password, and generates a JWT. Login verifies the user's email and password and, upon successful authentication, sends back a JWT. This token is subsequently used by the middleware for authentication.

Signup: This function handles new user registration. It takes user information, checks if the email is not already taken, hashes the password, and saves the user to the database. Finally, it returns a JWT token.

Login: This function is used for user login. It receives the email and password, and if they are valid, it generates a JWT token.

package controller

import (
    "context"
    "crud-api-go/model"
    "crud-api-go/utils"
    "net/http"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "golang.org/x/crypto/bcrypt"
)

func Signup(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var input model.SignupInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    var existingUser model.User
    err := getUserCollection().FindOne(ctx, bson.M{"email": input.Email}).Decode(&existingUser)
    if err == nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "User already exists"})
        return
    }

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
        return
    }

    newUser := model.User{
        ID:       primitive.NewObjectID(),
        Name:     input.Name,
        Email:    strings.ToLower(input.Email),
        Password: string(hashedPassword),
    }

    _, err = getUserCollection().InsertOne(ctx, newUser)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
        return
    }

    token, err := utils.GenerateJWT(newUser.Email)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create token"})
        return
    }

    c.JSON(http.StatusCreated, model.AuthResponse{
        Token: token,
        User: model.UserResponse{
            ID:    newUser.ID,
            Name:  newUser.Name,
            Email: newUser.Email,
        },
    })
}


func Login(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var input model.LoginInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
        return
    }

    var foundUser model.User
    err := getUserCollection().FindOne(ctx, bson.M{"email": input.Email}).Decode(&foundUser)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        return
    }

    err = bcrypt.CompareHashAndPassword([]byte(foundUser.Password), []byte(input.Password))
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials. Please check your email and password"})
        return
    }

    token, err := utils.GenerateJWT(foundUser.Email)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create token"})
        return
    }

    c.JSON(http.StatusOK, model.AuthResponse{
        Token: token,
        User: model.UserResponse{
            ID:    foundUser.ID,
            Name:  foundUser.Name,
            Email: foundUser.Email,
        },
    })
}

Enter fullscreen mode Exit fullscreen mode

7. go-crud-api/middleware/authMiddleware.go

Here we createa middleware to protect sensitive routes that require authentication, using JWT tokens.
This middleware (AuthMiddleware()) checks the Authorization header. If it's in the Bearer <token> format, it extracts the token, validates it using the JWT secret, and if valid, adds the user's email to the context for subsequent use. If there's an issue, the request is aborted.

package middleware

import (
    "crud-api-go/utils"
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
            c.Abort()
            return
        }
        const bearerPrefix = "Bearer "
        if !strings.HasPrefix(authHeader, bearerPrefix) {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
            c.Abort()
            return
        }
        tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
        claims, err := utils.ValidateToken(tokenString)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }
        c.Set("userEmail", claims.Email)
        c.Next()
    }
}

Enter fullscreen mode Exit fullscreen mode

8. ‍‍go-crud-api/utils/jwt.go

This file contains two key functions for working with JWT tokens.

  • GenerateJWT is used to create a token with the user's email and a 24-hour expiration time. ‍
  • ValidateToken is used to verify the token's validity and retrieve the information it contains. The HS256 algorithm and an environment variable are used for signing to enhance security.
package utils

import (
    "os"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var jwtKey = []byte(os.Getenv("JWT_SECRET"))

type Claims struct {
    Email string `json:"email"`
    jwt.RegisteredClaims
}

// Generate token
func GenerateJWT(email string) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)

    claims := &Claims{
        Email: email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(jwtKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

// validate Token
func ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}

    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return jwtKey, nil
    })

    if err != nil {
        return nil, err
    }

    if !token.Valid {
        return nil, jwt.ErrTokenInvalidClaims
    }

    return claims, nil
}

Enter fullscreen mode Exit fullscreen mode

Now We've reached the end of this tutorial! I hope this guide has been helpful for you in building a simple and secure API with Go, Gin, MongoDB, and JWT.
go-crud-api

Thank you for reading! πŸ€

Top comments (0)