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
π¦ 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
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
π§ 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")
}
The
main
function is the entry point of our program.config.ConnectDB()
: this line calls a function(ConnectDB
)fromconfig
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 functionuserRoutes
from the routes package,passing theGin 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 theGin
web server makes it listen for incoming HTTP requests on the addresslocalhost: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 variableDB
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 thegodotenv.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")
}
If the .env file failed to load the program execution will be halted.
mongoURI := os.Getenv("MONGO_URI")
databaseName := os.Getenv("DB_NAME")
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()
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)
}
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)
}
}
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"`
}
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 theGin
framework for handlingHTTP
requests,config
for database access andmodel
for defining data structures.Helper Function and Validation
func getUserCollection() *mongo.Collection {
return config.DB.Collection("users")
}
This function retrieves the MongoDB
collection named users
from the existing DB connection in the config
package.
var validate = validator.New()
We use the 'validator' package to check user inputs (e.g., whether fields are required or the email structure is correct).
GetUsers
: TheGetUsers
function retrieves a list of users from the database and responds inJSON
format.GetUserByID
: Retrieving a specific user from the database by ID and responding in JSON format.CreateUser
: Creating a new user and saving it toMongoDB
.UpdateUser
: Updating user information in the database based on theID
.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"})
}
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,
},
})
}
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()
}
}
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. TheHS256 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
}
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)