Back to all posts
October 12, 2025Charlie BrownDevelopment

Building REST APIs with Golang and Gin: A Complete Guide

Learn how to build high-performance REST APIs using Golang and the Gin framework, including routing, middleware, validation, and database integration.

Building REST APIs with Golang and Gin: A Complete Guide

Building REST APIs with Golang and Gin: A Complete Guide

Golang has become a popular choice for building REST APIs due to its performance, simplicity, and excellent concurrency support. When combined with the Gin web framework, you can create robust, scalable APIs quickly. In this article, we'll explore how to build a complete REST API using Golang and Gin.

Why Golang for APIs?

Golang offers several advantages for API development:

  • Performance: Compiled language with near-C performance
  • Concurrency: Built-in goroutines for handling concurrent requests
  • Simplicity: Clean syntax and minimal boilerplate
  • Standard Library: Rich standard library for HTTP, JSON, and more
  • Fast Compilation: Quick build times

Project Setup

1. Initialize Go Module

bash
mkdir my-api
cd my-api
go mod init github.com/yourusername/my-api

2. Install Dependencies

bash
go get -u github.com/gin-gonic/gin
go get -u github.com/gin-gonic/gin/binding
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/go-playground/validator/v10

Basic Gin Server

Start with a simple server:

go
// main.go
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // Create Gin router
    r := gin.Default()

    // Health check endpoint
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
            "message": "API is running",
        })
    })

    // Start server
    r.Run(":8080")
}

Routing and Handlers

RESTful Routes

go
// routes.go
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func setupRoutes(r *gin.Engine) {
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        {
            users.GET("", getUsers)           // GET /api/v1/users
            users.GET("/:id", getUser)         // GET /api/v1/users/:id
            users.POST("", createUser)         // POST /api/v1/users
            users.PUT("/:id", updateUser)      // PUT /api/v1/users/:id
            users.DELETE("/:id", deleteUser)   // DELETE /api/v1/users/:id
        }
    }
}

func getUsers(c *gin.Context) {
    // Implementation
    c.JSON(http.StatusOK, gin.H{
        "users": []string{},
    })
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, gin.H{
        "id": id,
    })
}

func createUser(c *gin.Context) {
    c.JSON(http.StatusCreated, gin.H{
        "message": "User created",
    })
}

func updateUser(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, gin.H{
        "id": id,
        "message": "User updated",
    })
}

func deleteUser(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, gin.H{
        "id": id,
        "message": "User deleted",
    })
}

Request Validation

Use struct tags for validation:

go
// models.go
package main

import (
    "time"
    "github.com/go-playground/validator/v10"
)

type User struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    FirstName string    `json:"first_name" binding:"required,min=2,max=50"`
    LastName  string    `json:"last_name" binding:"required,min=2,max=50"`
    Email     string    `json:"email" binding:"required,email"`
    Age       int       `json:"age" binding:"gte=18,lte=100"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

type CreateUserRequest struct {
    FirstName string `json:"first_name" binding:"required,min=2,max=50"`
    LastName  string `json:"last_name" binding:"required,min=2,max=50"`
    Email     string `json:"email" binding:"required,email"`
    Age       int    `json:"age" binding:"gte=18,lte=100"`
}

func createUser(c *gin.Context) {
    var req CreateUserRequest
    
    // Bind and validate
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // Create user
    user := User{
        FirstName: req.FirstName,
        LastName:  req.LastName,
        Email:     req.Email,
        Age:       req.Age,
    }

    // Save to database (example)
    c.JSON(http.StatusCreated, gin.H{
        "message": "User created",
        "user": user,
    })
}

Middleware

Create custom middleware for authentication, logging, and CORS:

go
// middleware.go
package main

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
)

// Authentication middleware
func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Authorization header required",
            })
            c.Abort()
            return
        }

        // Extract token (Bearer <token>)
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid authorization header format",
            })
            c.Abort()
            return
        }

        token := parts[1]
        
        // Validate token (implement your JWT validation)
        // For now, just store it in context
        c.Set("token", token)
        c.Next()
    }
}

// Logging middleware
func loggingMiddleware() gin.HandlerFunc {
    return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
            param.ClientIP,
            param.TimeStamp.Format(time.RFC1123),
            param.Method,
            param.Path,
            param.Request.Proto,
            param.StatusCode,
            param.Latency,
            param.Request.UserAgent(),
            param.ErrorMessage,
        )
    })
}

// CORS middleware
func corsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }

        c.Next()
    }
}

Database Integration with GORM

Integrate PostgreSQL using GORM:

go
// database.go
package main

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

var DB *gorm.DB

func initDB() {
    dsn := "host=localhost user=postgres password=postgres dbname=myapi port=5432 sslmode=disable TimeZone=Asia/Shanghai"
    
    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    
    if err != nil {
        panic("Failed to connect to database")
    }

    // Auto migrate
    DB.AutoMigrate(&User{})
}

// Repository pattern
type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *User) error {
    return r.db.Create(user).Error
}

func (r *UserRepository) FindByID(id uint) (*User, error) {
    var user User
    err := r.db.First(&user, id).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *UserRepository) FindAll(limit, offset int) ([]User, error) {
    var users []User
    err := r.db.Limit(limit).Offset(offset).Find(&users).Error
    return users, err
}

func (r *UserRepository) Update(user *User) error {
    return r.db.Save(user).Error
}

func (r *UserRepository) Delete(id uint) error {
    return r.db.Delete(&User{}, id).Error
}

Complete Handler Implementation

go
// handlers.go
package main

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    repo *UserRepository
}

func NewUserHandler(repo *UserRepository) *UserHandler {
    return &UserHandler{repo: repo}
}

func (h *UserHandler) GetUsers(c *gin.Context) {
    limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
    offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))

    users, err := h.repo.FindAll(limit, offset)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to fetch users",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "users": users,
        "limit": limit,
        "offset": offset,
    })
}

func (h *UserHandler) GetUser(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid user ID",
        })
        return
    }

    user, err := h.repo.FindByID(uint(id))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "User not found",
        })
        return
    }

    c.JSON(http.StatusOK, user)
}

func (h *UserHandler) CreateUser(c *gin.Context) {
    var req CreateUserRequest
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    user := User{
        FirstName: req.FirstName,
        LastName:  req.LastName,
        Email:     req.Email,
        Age:       req.Age,
    }

    if err := h.repo.Create(&user); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to create user",
        })
        return
    }

    c.JSON(http.StatusCreated, user)
}

Error Handling

Create a consistent error response format:

go
// errors.go
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

func handleError(c *gin.Context, err error) {
    switch err {
    case gorm.ErrRecordNotFound:
        c.JSON(http.StatusNotFound, APIError{
            Code:    http.StatusNotFound,
            Message: "Resource not found",
        })
    default:
        c.JSON(http.StatusInternalServerError, APIError{
            Code:    http.StatusInternalServerError,
            Message: "Internal server error",
            Details: err.Error(),
        })
    }
}

Complete Main Function

go
// main.go
package main

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

func main() {
    // Initialize database
    initDB()

    // Create Gin router
    r := gin.Default()

    // Apply middleware
    r.Use(loggingMiddleware())
    r.Use(corsMiddleware())

    // Setup routes
    setupRoutes(r)

    // Initialize handlers
    userRepo := NewUserRepository(DB)
    userHandler := NewUserHandler(userRepo)

    // Register routes
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        {
            users.GET("", userHandler.GetUsers)
            users.GET("/:id", userHandler.GetUser)
            users.POST("", userHandler.CreateUser)
            users.PUT("/:id", userHandler.UpdateUser)
            users.DELETE("/:id", userHandler.DeleteUser)
        }
    }

    // Start server
    r.Run(":8080")
}

Testing

Write tests for your handlers:

go
// handlers_test.go
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestCreateUser(t *testing.T) {
    gin.SetMode(gin.TestMode)
    
    r := gin.New()
    r.POST("/api/v1/users", createUser)

    reqBody := CreateUserRequest{
        FirstName: "John",
        LastName:  "Doe",
        Email:     "john@example.com",
        Age:       30,
    }
    
    jsonValue, _ := json.Marshal(reqBody)
    req, _ := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonValue))
    req.Header.Set("Content-Type", "application/json")

    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusCreated, w.Code)
}

Best Practices

  1. Use Repository Pattern: Separate data access logic from handlers
  2. Validate Input: Always validate and sanitize user input
  3. Error Handling: Consistent error response format
  4. Middleware: Use middleware for cross-cutting concerns
  5. Context: Use context for request-scoped data
  6. Testing: Write tests for all handlers
  7. Documentation: Use Swagger/OpenAPI for API documentation

Conclusion

Golang and Gin provide an excellent foundation for building REST APIs. With proper structure, middleware, validation, and database integration, you can create scalable, maintainable APIs. The combination of Golang's performance and Gin's simplicity makes it an ideal choice for modern API development.

References

Want more insights?

Subscribe to our newsletter or follow us for more updates on software development and team scaling.

Contact Us