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
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
mkdir my-api
cd my-api
go mod init github.com/yourusername/my-api2. Install Dependencies
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/v10Basic Gin Server
Start with a simple server:
// 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
// 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:
// 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:
// 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:
// 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
// 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:
// 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
// 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:
// 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
- Use Repository Pattern: Separate data access logic from handlers
- Validate Input: Always validate and sanitize user input
- Error Handling: Consistent error response format
- Middleware: Use middleware for cross-cutting concerns
- Context: Use context for request-scoped data
- Testing: Write tests for all handlers
- 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