skip to content
Astro Cactus

Building Web Applications with Gin

/ 8 min read

Building Web Applications with Gin

This guide demonstrates how to build high-performance web applications using the Gin framework in Go. We’ll create a complete REST API with best practices and production-ready features.

What is Gin?

Gin is a high-performance HTTP web framework written in Go. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, Gin is your best choice.

Example Project: Task Management API

We’ll build a task management API with the following features:

  1. CRUD operations for tasks
  2. User authentication
  3. Request validation
  4. Custom middleware
  5. File upload/download
  6. Error handling
  7. Swagger documentation

Project Structure

Terminal window
task-api/
├── handlers/
└── task.go
├── middleware/
└── auth.go
├── models/
└── task.go
├── docs/
└── swagger.json
├── main.go
└── go.mod

1. Models

models/task.go
package models
import "time"
type Task struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,min=3"`
Description string `json:"description"`
Status string `json:"status" binding:"required,oneof=pending in_progress completed"`
DueDate time.Time `json:"due_date" binding:"required,gtefield=time.Now"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
UserID uint `json:"user_id"`
}
type TaskFilter struct {
Status string `form:"status"`
Search string `form:"search"`
}

2. Middleware

middleware/auth.go
package middleware
import (
"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 required"})
c.Abort()
return
}
// Simple token validation (in production, use proper JWT validation)
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
c.Abort()
return
}
// Set user ID (in production, extract from JWT)
c.Set("user_id", uint(1))
c.Next()
}
}

3. Handlers

handlers/task.go
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"task-api/models"
)
type TaskHandler struct {
tasks []models.Task // In production, use a database
}
func NewTaskHandler() *TaskHandler {
return &TaskHandler{
tasks: make([]models.Task, 0),
}
}
// @Summary Create a new task
// @Description Create a new task with the provided details
// @Tags tasks
// @Accept json
// @Produce json
// @Param task body models.Task true "Task object"
// @Success 201 {object} models.Task
// @Failure 400 {object} map[string]string
// @Router /tasks [post]
func (h *TaskHandler) CreateTask(c *gin.Context) {
var task models.Task
if err := c.ShouldBindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Set metadata
task.ID = uint(len(h.tasks) + 1)
task.CreatedAt = time.Now()
task.UpdatedAt = time.Now()
task.UserID = c.GetUint("user_id")
h.tasks = append(h.tasks, task)
c.JSON(http.StatusCreated, task)
}
// @Summary Get all tasks
// @Description Get all tasks with optional filtering
// @Tags tasks
// @Accept json
// @Produce json
// @Param status query string false "Filter by status"
// @Param search query string false "Search in title"
// @Success 200 {array} models.Task
// @Router /tasks [get]
func (h *TaskHandler) ListTasks(c *gin.Context) {
var filter models.TaskFilter
if err := c.ShouldBindQuery(&filter); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := c.GetUint("user_id")
var filteredTasks []models.Task
for _, task := range h.tasks {
if task.UserID != userID {
continue
}
if filter.Status != "" && task.Status != filter.Status {
continue
}
if filter.Search != "" && !strings.Contains(
strings.ToLower(task.Title),
strings.ToLower(filter.Search),
) {
continue
}
filteredTasks = append(filteredTasks, task)
}
c.JSON(http.StatusOK, filteredTasks)
}
// @Summary Get task by ID
// @Description Get a task by its ID
// @Tags tasks
// @Accept json
// @Produce json
// @Param id path int true "Task ID"
// @Success 200 {object} models.Task
// @Failure 404 {object} map[string]string
// @Router /tasks/{id} [get]
func (h *TaskHandler) GetTask(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
userID := c.GetUint("user_id")
for _, task := range h.tasks {
if task.ID == uint(id) && task.UserID == userID {
c.JSON(http.StatusOK, task)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
}
// @Summary Update task
// @Description Update an existing task
// @Tags tasks
// @Accept json
// @Produce json
// @Param id path int true "Task ID"
// @Param task body models.Task true "Task object"
// @Success 200 {object} models.Task
// @Failure 404 {object} map[string]string
// @Router /tasks/{id} [put]
func (h *TaskHandler) UpdateTask(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
var updatedTask models.Task
if err := c.ShouldBindJSON(&updatedTask); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := c.GetUint("user_id")
for i, task := range h.tasks {
if task.ID == uint(id) && task.UserID == userID {
updatedTask.ID = task.ID
updatedTask.CreatedAt = task.CreatedAt
updatedTask.UpdatedAt = time.Now()
updatedTask.UserID = userID
h.tasks[i] = updatedTask
c.JSON(http.StatusOK, updatedTask)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
}
// @Summary Delete task
// @Description Delete a task by ID
// @Tags tasks
// @Accept json
// @Produce json
// @Param id path int true "Task ID"
// @Success 204
// @Failure 404 {object} map[string]string
// @Router /tasks/{id} [delete]
func (h *TaskHandler) DeleteTask(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
userID := c.GetUint("user_id")
for i, task := range h.tasks {
if task.ID == uint(id) && task.UserID == userID {
h.tasks = append(h.tasks[:i], h.tasks[i+1:]...)
c.Status(http.StatusNoContent)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
}
// @Summary Upload task attachment
// @Description Upload a file attachment for a task
// @Tags tasks
// @Accept multipart/form-data
// @Produce json
// @Param id path int true "Task ID"
// @Param file formData file true "File to upload"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Router /tasks/{id}/attachment [post]
func (h *TaskHandler) UploadAttachment(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
return
}
// Save file (in production, use proper storage service)
filename := fmt.Sprintf("task_%d_%s", id, file.Filename)
if err := c.SaveUploadedFile(file, filename); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "File uploaded successfully",
"filename": filename,
})
}

4. Main Application

main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
_ "task-api/docs" // This is required for swagger
"task-api/handlers"
"task-api/middleware"
)
// @title Task Management API
// @version 1.0
// @description This is a task management server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api
func main() {
r := gin.Default()
// Add global middleware
r.Use(gin.Recovery())
r.Use(gin.Logger())
// Create handler
taskHandler := handlers.NewTaskHandler()
// Swagger documentation
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Public routes
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "healthy"})
})
// Protected routes
api := r.Group("/api")
api.Use(middleware.AuthMiddleware())
{
// Task routes
tasks := api.Group("/tasks")
{
tasks.POST("", taskHandler.CreateTask)
tasks.GET("", taskHandler.ListTasks)
tasks.GET("/:id", taskHandler.GetTask)
tasks.PUT("/:id", taskHandler.UpdateTask)
tasks.DELETE("/:id", taskHandler.DeleteTask)
tasks.POST("/:id/attachment", taskHandler.UploadAttachment)
}
}
// Start server
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}

Gin Features and Best Practices

1. Middleware

  • Use middleware for cross-cutting concerns
  • Chain middleware appropriately
  • Keep middleware focused and reusable
// Custom logging middleware
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// Process request
c.Next()
// Log request details
duration := time.Since(start)
log.Printf(
"%s %s %d %s",
c.Request.Method,
c.Request.URL.Path,
c.Writer.Status(),
duration,
)
}
}

2. Request Validation

  • Use binding tags for validation
  • Create custom validators when needed
  • Validate at the handler level
// Custom validator
type Task struct {
Priority int `binding:"required,gte=1,lte=5"`
}
// Register custom validator
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("priority", validatePriority)
}

3. Error Handling

  • Use proper HTTP status codes
  • Return consistent error responses
  • Implement error middleware
// Error middleware
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
c.JSON(http.StatusBadRequest, gin.H{
"errors": c.Errors.Errors(),
})
}
}
}

4. Route Groups

  • Organize routes logically
  • Use versioning
  • Apply middleware to groups
v1 := r.Group("/api/v1")
v1.Use(AuthMiddleware())
{
tasks := v1.Group("/tasks")
{
tasks.GET("", handler.ListTasks)
tasks.POST("", handler.CreateTask)
}
}

5. Response Formatting

  • Use consistent response format
  • Include metadata when needed
  • Handle pagination properly
type Response struct {
Data interface{} `json:"data"`
Message string `json:"message,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type Meta struct {
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}

Production Considerations

  1. Configuration
type Config struct {
Port string `env:"PORT" envDefault:"8080"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
Mode string `env:"GIN_MODE" envDefault:"release"`
}
  1. Logging
// Use structured logging
r.Use(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,
)
}))
  1. Security Headers
r.Use(secure.New(secure.Options{
AllowedHosts: []string{"example.com"},
SSLRedirect: true,
STSSeconds: 31536000,
STSIncludeSubdomains: true,
FrameDeny: true,
ContentTypeNosniff: true,
BrowserXssFilter: true,
}))
  1. Rate Limiting
limiter := rate.NewLimiter(rate.Every(time.Second), 10)
r.Use(func(c *gin.Context) {
if !limiter.Allow() {
c.AbortWithStatus(http.StatusTooManyRequests)
return
}
c.Next()
})
  1. Graceful Shutdown
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}

This guide demonstrates how to build a production-ready web application using Gin, following best practices for structure, error handling, validation, and security.