Gin API Structure
When building RESTful APIs with Go's Gin framework, establishing a clear and maintainable structure is essential for your application's long-term success. This guide will walk you through how to organize your Gin API project, from basic routing to advanced architectural patterns.
Introduction to API Structure
A well-structured API makes development faster, maintenance easier, and onboarding new developers smoother. For Gin applications, we typically organize code around:
- Routes and handlers
- Middleware layers
- Controllers/services
- Models/data structures
- Configuration management
Let's explore how these components fit together in a Gin application.
Basic Structure of a Gin Application
Here's a simplified diagram of how a Gin application is structured:
Client Request → Router → Middleware → Handler → Response
A basic Gin application starts with this structure:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// Initialize Gin router
router := gin.Default()
// Define a simple route
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// Start the server
router.Run(":8080")
}
This example shows the minimal structure: a router with a single route handling GET requests to "/ping".
Organizing Routes
As your API grows, inline route definitions become unmanageable. Let's organize routes by resource:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Group routes by version and resource
v1 := router.Group("/api/v1")
{
users := v1.Group("/users")
{
users.GET("/", listUsers)
users.GET("/:id", getUserByID)
users.POST("/", createUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
products := v1.Group("/products")
{
products.GET("/", listProducts)
// other product routes...
}
}
router.Run(":8080")
}
// Handler functions
func listUsers(c *gin.Context) {
// Implementation
}
func getUserByID(c *gin.Context) {
// Implementation
}
// Other handler functions...
This approach groups related endpoints together, making the code more organized and easier to navigate.
Project Directory Structure
For larger applications, consider this directory structure:
/project-root
/cmd
/api
main.go # Entry point
/internal
/api
/handlers # Request handlers
/middleware # Custom middleware
/models # Data models
/repositories # Data access
/services # Business logic
/pkg
/config # Configuration
/utils # Utilities
/migrations # Database migrations
/configs # Configuration files
go.mod
go.sum
Let's see how to implement this structure with a practical example.
Complete Example: User API
Let's build a simple user API with proper structure:
1. Models (internal/api/models/user.go)
package models
import "time"
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
2. Repositories (internal/api/repositories/user_repository.go)
package repositories
import (
"your-project/internal/api/models"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindAll() ([]models.User, error) {
var users []models.User
result := r.db.Find(&users)
return users, result.Error
}
func (r *UserRepository) FindByID(id uint) (models.User, error) {
var user models.User
result := r.db.First(&user, id)
return user, result.Error
}
func (r *UserRepository) Create(user models.User) (models.User, error) {
result := r.db.Create(&user)
return user, result.Error
}
// Other repository methods...
3. Services (internal/api/services/user_service.go)
package services
import (
"your-project/internal/api/models"
"your-project/internal/api/repositories"
)
type UserService struct {
userRepo *repositories.UserRepository
}
func NewUserService(userRepo *repositories.UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) GetAllUsers() ([]models.UserResponse, error) {
users, err := s.userRepo.FindAll()
if err != nil {
return nil, err
}
var response []models.UserResponse
for _, user := range users {
response = append(response, models.UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
})
}
return response, nil
}
func (s *UserService) CreateUser(req models.CreateUserRequest) (models.UserResponse, error) {
user := models.User{
Name: req.Name,
Email: req.Email,
}
createdUser, err := s.userRepo.Create(user)
if err != nil {
return models.UserResponse{}, err
}
return models.UserResponse{
ID: createdUser.ID,
Name: createdUser.Name,
Email: createdUser.Email,
}, nil
}
// Other service methods...
4. Handlers (internal/api/handlers/user_handler.go)
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"your-project/internal/api/models"
"your-project/internal/api/services"
)
type UserHandler struct {
userService *services.UserService
}
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) GetUsers(c *gin.Context) {
users, err := h.userService.GetAllUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
}
func (h *UserHandler) GetUserByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := h.userService.GetUserByID(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 request models.CreateUserRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.CreateUser(request)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// Other handler methods...
5. Router Setup (internal/api/router.go)
package api
import (
"github.com/gin-gonic/gin"
"your-project/internal/api/handlers"
"your-project/internal/api/middleware"
)
func SetupRouter(
userHandler *handlers.UserHandler,
authMiddleware *middleware.AuthMiddleware,
) *gin.Engine {
router := gin.Default()
// Apply global middleware
router.Use(middleware.CORSMiddleware())
// API version group
v1 := router.Group("/api/v1")
{
// Public routes
v1.POST("/login", authHandler.Login)
// Protected routes
protected := v1.Group("/")
protected.Use(authMiddleware.RequireAuth())
{
users := protected.Group("/users")
{
users.GET("/", userHandler.GetUsers)
users.POST("/", userHandler.CreateUser)
users.GET("/:id", userHandler.GetUserByID)
users.PUT("/:id", userHandler.UpdateUser)
users.DELETE("/:id", userHandler.DeleteUser)
}
}
}
return router
}
6. Main Application (cmd/api/main.go)
package main
import (
"log"
"your-project/internal/api"
"your-project/internal/api/handlers"
"your-project/internal/api/middleware"
"your-project/internal/api/repositories"
"your-project/internal/api/services"
"your-project/internal/pkg/config"
"your-project/internal/pkg/database"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Setup database connection
db, err := database.Connect(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Initialize repositories
userRepo := repositories.NewUserRepository(db)
// Initialize services
userService := services.NewUserService(userRepo)
// Initialize handlers
userHandler := handlers.NewUserHandler(userService)
// Initialize middleware
authMiddleware := middleware.NewAuthMiddleware(cfg.JWTSecret)
// Setup router
router := api.SetupRouter(userHandler, authMiddleware)
// Start the server
log.Printf("Server starting on port %s", cfg.Port)
router.Run(":" + cfg.Port)
}
Best Practices for Gin API Structure
-
Separation of Concerns: Keep routes, business logic, and data access separate.
-
Use Dependency Injection: Pass dependencies to components rather than creating them internally. This improves testability.
-
Consistent Error Handling: Create a standard error response format across your API.
func errorResponse(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"error": message,
})
}
-
Middleware for Cross-Cutting Concerns: Use middleware for authentication, logging, CORS, etc.
-
Versioning: Include API version in the URL path to allow for future changes.
-
Response Format Consistency: Standardize how your API formats responses.
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func successResponse(c *gin.Context, code int, message string, data interface{}) {
c.JSON(code, Response{
Status: "success",
Message: message,
Data: data,
})
}
Common Middleware for Gin APIs
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// CORSMiddleware handles Cross-Origin Resource Sharing
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Authorization, Content-Type")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// LoggerMiddleware logs request information
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Process request
c.Next()
// Log after request is processed
statusCode := c.Writer.Status()
path := c.Request.URL.Path
method := c.Request.Method
// Use your preferred logger here
fmt.Printf("[%d] %s %s\n", statusCode, method, path)
}
}
Summary
A well-structured Gin API follows these key principles:
- Organize code by responsibility (models, repositories, services, handlers)
- Group related routes together
- Use middleware for cross-cutting concerns
- Maintain consistent response formats and error handling
- Implement proper dependency injection
This modular approach makes your Gin applications more maintainable, testable, and scalable. As your API grows, this structure provides clear places for new functionality without creating a tangled mess of code.
Additional Resources
- Official Gin Documentation
- Go Project Layout - For standardized project organization
- GORM Documentation - For database operations
Exercises
- Create a simple blog API with posts and comments resources using the structure outlined in this guide.
- Add custom middleware that logs the time taken to process each request.
- Implement a standard error handling system that returns appropriate HTTP status codes and consistent error messages.
- Create a health check endpoint that verifies database connectivity and returns the API status.
- Extend the user API example with search and pagination functionality.
By following these patterns, you'll build Gin APIs that are robust, maintainable, and ready to grow with your application's needs.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!