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:
- CRUD operations for tasks
- User authentication
- Request validation
- Custom middleware
- File upload/download
- Error handling
- Swagger documentation
Project Structure
task-api/├── handlers/│ └── task.go├── middleware/│ └── auth.go├── models/│ └── task.go├── docs/│ └── swagger.json├── main.go└── go.mod1. Models
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
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
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
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 /apifunc 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 middlewarefunc 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 validatortype Task struct { Priority int `binding:"required,gte=1,lte=5"`}
// Register custom validatorif 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 middlewarefunc 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
- Configuration
type Config struct { Port string `env:"PORT" envDefault:"8080"` LogLevel string `env:"LOG_LEVEL" envDefault:"info"` Mode string `env:"GIN_MODE" envDefault:"release"`}- Logging
// Use structured loggingr.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, )}))- Security Headers
r.Use(secure.New(secure.Options{ AllowedHosts: []string{"example.com"}, SSLRedirect: true, STSSeconds: 31536000, STSIncludeSubdomains: true, FrameDeny: true, ContentTypeNosniff: true, BrowserXssFilter: true,}))- 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()})- 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.