Gin Framework HTTP Routes

Gin Framework HTTP Routes

Gin is a popular web framework for Go that makes it easy to build web applications and APIs. This tutorial covers how to create and manage HTTP routes in Gin.

What is Gin?

Gin is a high-performance HTTP web framework written in Go. It provides a martini-like API with much better performance and productivity.

Installation

1. Install Gin

go get -u github.com/gin-gonic/gin

2. Create Your First Gin App

// main.go
package main

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

func main() {
    // Create a Gin router
    r := gin.Default()
    
    // Define a route
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "Hello, World!",
        })
    })
    
    // Start the server
    r.Run(":8080")
}

Run the application:

go run main.go

Basic Routing

1. HTTP Methods

package main

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

func main() {
    r := gin.Default()
    
    // GET route
    r.GET("/users", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "method": "GET",
            "message": "Get all users",
        })
    })
    
    // POST route
    r.POST("/users", func(c *gin.Context) {
        c.JSON(http.StatusCreated, gin.H{
            "method": "POST",
            "message": "Create a new user",
        })
    })
    
    // PUT route
    r.PUT("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "method": "PUT",
            "id":     id,
            "message": "Update user",
        })
    })
    
    // DELETE route
    r.DELETE("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "method": "DELETE",
            "id":     id,
            "message": "Delete user",
        })
    })
    
    // PATCH route
    r.PATCH("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "method": "PATCH",
            "id":     id,
            "message": "Partially update user",
        })
    })
    
    r.Run(":8080")
}

2. Any Method

// Handle any HTTP method
r.Any("/test", func(c *gin.Context) {
    method := c.Request.Method
    c.JSON(http.StatusOK, gin.H{
        "method": method,
        "message": "This endpoint accepts any HTTP method",
    })
})

// Handle all methods (catch-all)
r.NoRoute(func(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{
        "error": "Route not found",
    })
})

Route Parameters

1. Path Parameters

package main

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

func main() {
    r := gin.Default()
    
    // Single parameter
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{
            "user_id": id,
            "message": "Get user by ID",
        })
    })
    
    // Multiple parameters
    r.GET("/users/:id/posts/:post_id", func(c *gin.Context) {
        userID := c.Param("id")
        postID := c.Param("post_id")
        c.JSON(http.StatusOK, gin.H{
            "user_id":  userID,
            "post_id":  postID,
            "message":  "Get user post",
        })
    })
    
    // Wildcard parameter
    r.GET("/files/*filepath", func(c *gin.Context) {
        filepath := c.Param("filepath")
        c.JSON(http.StatusOK, gin.H{
            "filepath": filepath,
            "message":  "Get file",
        })
    })
    
    r.Run(":8080")
}

2. Query Parameters

// Query parameters
r.GET("/search", func(c *gin.Context) {
    query := c.Query("q")
    page := c.DefaultQuery("page", "1")
    limit := c.DefaultQuery("limit", "10")
    
    c.JSON(http.StatusOK, gin.H{
        "query": query,
        "page":  page,
        "limit": limit,
    })
})

// Get all query parameters
r.GET("/debug", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "method": c.Request.Method,
        "path":   c.Request.URL.Path,
        "query":  c.Request.URL.RawQuery,
        "params": c.Request.URL.Query(),
    })
})

Route Groups

1. Basic Route Groups

package main

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

func main() {
    r := gin.Default()
    
    // API v1 group
    v1 := r.Group("/api/v1")
    {
        v1.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "version": "v1",
                "message": "Get all users",
            })
        })
        
        v1.POST("/users", func(c *gin.Context) {
            c.JSON(http.StatusCreated, gin.H{
                "version": "v1",
                "message": "Create user",
            })
        })
        
        v1.GET("/users/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(http.StatusOK, gin.H{
                "version": "v1",
                "user_id": id,
                "message": "Get user",
            })
        })
    }
    
    // API v2 group
    v2 := r.Group("/api/v2")
    {
        v2.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "version": "v2",
                "message": "Get all users (v2)",
            })
        })
    }
    
    r.Run(":8080")
}

2. Nested Route Groups

// Admin group with nested groups
admin := r.Group("/admin")
{
    // Admin users group
    users := admin.Group("/users")
    {
        users.GET("/", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "message": "Admin: Get all users",
            })
        })
        
        users.POST("/", func(c *gin.Context) {
            c.JSON(http.StatusCreated, gin.H{
                "message": "Admin: Create user",
            })
        })
        
        // Admin user settings group
        settings := users.Group("/:id/settings")
        {
            settings.GET("/", func(c *gin.Context) {
                id := c.Param("id")
                c.JSON(http.StatusOK, gin.H{
                    "user_id": id,
                    "message": "Admin: Get user settings",
                })
            })
            
            settings.PUT("/", func(c *gin.Context) {
                id := c.Param("id")
                c.JSON(http.StatusOK, gin.H{
                    "user_id": id,
                    "message": "Admin: Update user settings",
                })
            })
        }
    }
}

Request Handling

1. JSON Request Body

package main

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

// User struct for JSON binding
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age"`
}

func main() {
    r := gin.Default()
    
    // Create user with JSON body
    r.POST("/users", func(c *gin.Context) {
        var user User
        
        // Bind JSON to struct
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        
        // Process user data
        user.ID = "123" // Generate ID in real app
        
        c.JSON(http.StatusCreated, gin.H{
            "message": "User created successfully",
            "user":    user,
        })
    })
    
    // Update user
    r.PUT("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        var user User
        
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        
        user.ID = id
        
        c.JSON(http.StatusOK, gin.H{
            "message": "User updated successfully",
            "user":    user,
        })
    })
    
    r.Run(":8080")
}

2. Form Data

// Handle form data
r.POST("/login", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.PostForm("password")
    
    if username == "" || password == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Username and password are required",
        })
        return
    }
    
    // Validate credentials (in real app, check against database)
    if username == "admin" && password == "password" {
        c.JSON(http.StatusOK, gin.H{
            "message": "Login successful",
            "token":   "fake-jwt-token",
        })
    } else {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Invalid credentials",
        })
    }
})

// Handle multipart form (file upload)
r.POST("/upload", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "No file uploaded",
        })
        return
    }
    
    // Save file (in real app, save to proper location)
    filename := file.Filename
    size := file.Size
    
    c.JSON(http.StatusOK, gin.H{
        "message":  "File uploaded successfully",
        "filename": filename,
        "size":     size,
    })
})

Middleware

1. Global Middleware

package main

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

// Logger middleware
func Logger() 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,
        )
    })
}

// Recovery middleware
func Recovery() gin.HandlerFunc {
    return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
        if err, ok := recovered.(string); ok {
            log.Printf("Panic recovered: %s", err)
        }
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Internal server error",
        })
    })
}

func main() {
    r := gin.New()
    
    // Add global middleware
    r.Use(Logger())
    r.Use(Recovery())
    
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello with middleware!",
        })
    })
    
    r.Run(":8080")
}

2. Route-Specific Middleware

// Authentication middleware
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        
        // Simple token validation (in real app, validate JWT)
        if token != "Bearer valid-token" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
            })
            c.Abort()
            return
        }
        
        // Set user context
        c.Set("user_id", "123")
        c.Next()
    }
}

// Apply middleware to specific routes
r.GET("/public", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "Public endpoint - no auth required",
    })
})

r.GET("/private", AuthMiddleware(), func(c *gin.Context) {
    userID := c.GetString("user_id")
    c.JSON(http.StatusOK, gin.H{
        "message": "Private endpoint - auth required",
        "user_id": userID,
    })
})

// Apply middleware to route groups
authorized := r.Group("/api")
authorized.Use(AuthMiddleware())
{
    authorized.GET("/profile", func(c *gin.Context) {
        userID := c.GetString("user_id")
        c.JSON(http.StatusOK, gin.H{
            "user_id": userID,
            "message": "User profile",
        })
    })
}

Response Handling

1. Different Response Types

// JSON response
r.GET("/json", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "JSON response",
        "status":  "success",
    })
})

// String response
r.GET("/string", func(c *gin.Context) {
    c.String(http.StatusOK, "String response: %s", "Hello, World!")
})

// HTML response
r.GET("/html", func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.html", gin.H{
        "title": "Gin Web Framework",
    })
})

// File response
r.GET("/file", func(c *gin.Context) {
    c.File("./static/file.txt")
})

// Download response
r.GET("/download", func(c *gin.Context) {
    c.Header("Content-Disposition", "attachment; filename=file.txt")
    c.File("./static/file.txt")
})

// XML response
r.GET("/xml", func(c *gin.Context) {
    c.XML(http.StatusOK, gin.H{
        "message": "XML response",
        "status":  "success",
    })
})

2. HTTP Status Codes

// Different status codes
r.GET("/status/:code", func(c *gin.Context) {
    code := c.Param("code")
    
    switch code {
    case "200":
        c.JSON(http.StatusOK, gin.H{"status": "OK"})
    case "201":
        c.JSON(http.StatusCreated, gin.H{"status": "Created"})
    case "400":
        c.JSON(http.StatusBadRequest, gin.H{"status": "Bad Request"})
    case "401":
        c.JSON(http.StatusUnauthorized, gin.H{"status": "Unauthorized"})
    case "403":
        c.JSON(http.StatusForbidden, gin.H{"status": "Forbidden"})
    case "404":
        c.JSON(http.StatusNotFound, gin.H{"status": "Not Found"})
    case "500":
        c.JSON(http.StatusInternalServerError, gin.H{"status": "Internal Server Error"})
    default:
        c.JSON(http.StatusOK, gin.H{"status": "Default"})
    }
})

Error Handling

1. Custom Error Handling

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

func (e APIError) Error() string {
    return e.Message
}

// Error handling middleware
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        // Check for errors
        if len(c.Errors) > 0 {
            err := c.Errors.Last().Err
            
            var apiError APIError
            switch e := err.(type) {
            case APIError:
                apiError = e
            default:
                apiError = APIError{
                    Code:    http.StatusInternalServerError,
                    Message: "Internal server error",
                    Details: e.Error(),
                }
            }
            
            c.JSON(apiError.Code, apiError)
        }
    }
}

// Route that returns an error
r.GET("/error", func(c *gin.Context) {
    c.Error(APIError{
        Code:    http.StatusBadRequest,
        Message: "This is a custom error",
        Details: "Additional error details",
    })
})

Static Files

1. Serving Static Files

// Serve static files
r.Static("/static", "./static")

// Serve single file
r.StaticFile("/favicon.ico", "./static/favicon.ico")

// Serve static files with custom URL
r.StaticFS("/assets", gin.Dir("./public", true))

Complete Example

package main

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

// User model
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age"`
}

// In-memory user storage
var users = map[string]User{
    "1": {ID: "1", Name: "John Doe", Email: "[email protected]", Age: 30},
    "2": {ID: "2", Name: "Jane Smith", Email: "[email protected]", Age: 25},
}

// Logger middleware
func Logger() gin.HandlerFunc {
    return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        return fmt.Sprintf("[%s] %s %s %d %s\n",
            param.TimeStamp.Format(time.RFC3339),
            param.Method,
            param.Path,
            param.StatusCode,
            param.Latency,
        )
    })
}

// Auth middleware
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token != "Bearer valid-token" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
            c.Abort()
            return
        }
        c.Next()
    }
}

func main() {
    // Set Gin mode
    gin.SetMode(gin.ReleaseMode)
    
    // Create router
    r := gin.New()
    
    // Add middleware
    r.Use(Logger())
    r.Use(gin.Recovery())
    
    // CORS middleware
    r.Use(func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        
        c.Next()
    })
    
    // Health check
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status":    "healthy",
            "timestamp": time.Now().Unix(),
            "version":   "1.0.0",
        })
    })
    
    // API routes
    api := r.Group("/api/v1")
    {
        // Public routes
        api.GET("/users", getAllUsers)
        api.GET("/users/:id", getUser)
        
        // Protected routes
        protected := api.Group("/")
        protected.Use(AuthMiddleware())
        {
            protected.POST("/users", createUser)
            protected.PUT("/users/:id", updateUser)
            protected.DELETE("/users/:id", deleteUser)
        }
    }
    
    // Start server
    r.Run(":8080")
}

// Handlers
func getAllUsers(c *gin.Context) {
    userList := make([]User, 0, len(users))
    for _, user := range users {
        userList = append(userList, user)
    }
    
    c.JSON(http.StatusOK, gin.H{
        "users": userList,
        "count": len(userList),
    })
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    
    user, exists := users[id]
    if !exists {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "user": user,
    })
}

func createUser(c *gin.Context) {
    var user User
    
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // Generate ID
    id := strconv.Itoa(len(users) + 1)
    user.ID = id
    users[id] = user
    
    c.JSON(http.StatusCreated, gin.H{
        "message": "User created successfully",
        "user":    user,
    })
}

func updateUser(c *gin.Context) {
    id := c.Param("id")
    
    _, exists := users[id]
    if !exists {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    user.ID = id
    users[id] = user
    
    c.JSON(http.StatusOK, gin.H{
        "message": "User updated successfully",
        "user":    user,
    })
}

func deleteUser(c *gin.Context) {
    id := c.Param("id")
    
    _, exists := users[id]
    if !exists {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    delete(users, id)
    
    c.JSON(http.StatusOK, gin.H{
        "message": "User deleted successfully",
    })
}

Testing with curl

# Health check
curl http://localhost:8080/health

# Get all users
curl http://localhost:8080/api/v1/users

# Get specific user
curl http://localhost:8080/api/v1/users/1

# Create user (with auth)
curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer valid-token" \
  -d '{"name":"Alice","email":"[email protected]","age":28}'

# Update user (with auth)
curl -X PUT http://localhost:8080/api/v1/users/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer valid-token" \
  -d '{"name":"John Updated","email":"[email protected]","age":31}'

# Delete user (with auth)
curl -X DELETE http://localhost:8080/api/v1/users/1 \
  -H "Authorization: Bearer valid-token"

External Resources:

Related Tutorials:

Last updated on