Gin Directory Structure
When building web applications with the Gin framework in Go, having a well-organized directory structure is crucial for maintainability, scalability, and collaboration. This guide will walk you through a recommended directory structure for Gin applications and explain the purpose of each component.
Introduction to Directory Structure
A well-planned directory structure provides several benefits:
- Maintainability: Makes it easier to find and update specific parts of your code
- Scalability: Allows your application to grow without becoming chaotic
- Collaboration: Helps team members understand where to find or add code
- Separation of Concerns: Keeps different aspects of your application isolated
Let's explore a recommended directory structure for Gin applications that follows industry best practices.
Basic Gin Project Structure
At its most basic level, here's a starter directory structure for a Gin project:
my-gin-app/
├── main.go
├── go.mod
├── go.sum
├── config/
├── controllers/
├── middlewares/
├── models/
├── routes/
├── services/
├── utils/
└── static/
Let's explain the purpose of each directory and file.
Main Entry Point: main.go
The main.go
file serves as the entry point for your Gin application. It typically:
- Initializes the Gin router
- Sets up configuration
- Registers routes
- Starts the HTTP server
Here's a simple example of a main.go
file:
package main
import (
"log"
"my-gin-app/config"
"my-gin-app/routes"
"github.com/gin-gonic/gin"
)
func main() {
// Set Gin mode
gin.SetMode(config.GetConfig().ServerMode)
// Initialize router
router := gin.Default()
// Register routes
routes.RegisterRoutes(router)
// Start server
port := config.GetConfig().ServerPort
log.Printf("Server starting on port %s", port)
router.Run(":" + port)
}
Module Dependencies: go.mod
and go.sum
These files manage your Go module and its dependencies:
go.mod
: Defines your module path and its dependenciesgo.sum
: Contains checksums for dependency verification
Here's a sample go.mod
file:
module my-gin-app
go 1.19
require (
github.com/gin-gonic/gin v1.8.2
github.com/go-sql-driver/mysql v1.7.0
github.com/joho/godotenv v1.5.1
)
Core Directories Explained
config/
The config/
directory contains configuration files and code for loading settings from various sources (environment variables, config files, etc.).
Example config/config.go
:
package config
import (
"os"
"github.com/joho/godotenv"
)
type Configuration struct {
ServerPort string
ServerMode string
DBHost string
DBUser string
DBPassword string
DBName string
}
var config Configuration
func init() {
// Load .env file if it exists
godotenv.Load()
config = Configuration{
ServerPort: getEnv("SERVER_PORT", "8080"),
ServerMode: getEnv("GIN_MODE", "debug"),
DBHost: getEnv("DB_HOST", "localhost"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "myapp"),
}
}
func GetConfig() Configuration {
return config
}
func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}
controllers/
Controllers handle HTTP requests and responses, processing user input and returning appropriate output. They call services to perform business logic and return formatted responses.
Example controllers/user_controller.go
:
package controllers
import (
"my-gin-app/models"
"my-gin-app/services"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// UserController handles user-related HTTP requests
type UserController struct {
userService services.UserService
}
// NewUserController creates a new user controller
func NewUserController(userService services.UserService) *UserController {
return &UserController{
userService: userService,
}
}
// GetUser retrieves a user by ID
func (c *UserController) GetUser(ctx *gin.Context) {
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, err := c.userService.GetByID(id)
if err != nil {
ctx.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
ctx.JSON(http.StatusOK, user)
}
// CreateUser creates a new user
func (c *UserController) CreateUser(ctx *gin.Context) {
var user models.User
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
createdUser, err := c.userService.Create(user)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusCreated, createdUser)
}
models/
The models/
directory contains data structures that represent your application's domain entities, often mapping to database tables.
Example models/user.go
:
package models
import "time"
// User represents a user in the system
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique;not null"`
Email string `json:"email" gorm:"unique;not null"`
Password string `json:"-" gorm:"not null"` // "-" prevents it from being shown in JSON responses
FullName string `json:"full_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
routes/
The routes/
directory defines and organizes your application's API endpoints, connecting URLs to controller handlers.
Example routes/routes.go
:
package routes
import (
"my-gin-app/controllers"
"my-gin-app/middlewares"
"my-gin-app/services"
"github.com/gin-gonic/gin"
)
// RegisterRoutes sets up all routes for the application
func RegisterRoutes(router *gin.Engine) {
// Create services
userService := services.NewUserService()
// Create controllers
userController := controllers.NewUserController(userService)
// Public routes
public := router.Group("/api")
{
// Authentication routes
public.POST("/login", controllers.Login)
public.POST("/register", controllers.Register)
// User routes
users := public.Group("/users")
{
users.GET("/:id", userController.GetUser)
}
}
// Protected routes (require authentication)
protected := router.Group("/api")
protected.Use(middlewares.Authenticate())
{
// User routes that require authentication
users := protected.Group("/users")
{
users.POST("/", userController.CreateUser)
users.PUT("/:id", userController.UpdateUser)
users.DELETE("/:id", userController.DeleteUser)
}
}
}
middlewares/
Middlewares intercept HTTP requests before they reach controllers. They handle cross-cutting concerns like authentication, logging, CORS, etc.
Example middlewares/auth.go
:
package middlewares
import (
"my-gin-app/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// Authenticate verifies the JWT token in the request header
func Authenticate() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
return
}
// Check if the header has the format "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer <token>"})
return
}
token := parts[1]
// Validate the token
claims, err := utils.ValidateToken(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
return
}
// Add claims to context so that they're accessible in handlers
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
services/
Services encapsulate business logic, separating it from HTTP concerns. They interact with repositories or directly with databases.
Example services/user_service.go
:
package services
import (
"errors"
"my-gin-app/models"
)
// UserService defines methods for user-related business operations
type UserService interface {
GetByID(id int) (*models.User, error)
GetByEmail(email string) (*models.User, error)
Create(user models.User) (*models.User, error)
Update(user models.User) (*models.User, error)
Delete(id int) error
}
// userService implements UserService
type userService struct {
// In a real application, you'd inject a repository here
// userRepo repositories.UserRepository
}
// NewUserService creates a new user service
func NewUserService() UserService {
return &userService{
// userRepo: repositories.NewUserRepository(),
}
}
// Mock implementation for demonstration purposes
var users = make(map[int]*models.User)
var nextID = 1
func (s *userService) GetByID(id int) (*models.User, error) {
if user, exists := users[id]; exists {
return user, nil
}
return nil, errors.New("user not found")
}
func (s *userService) GetByEmail(email string) (*models.User, error) {
for _, user := range users {
if user.Email == email {
return user, nil
}
}
return nil, errors.New("user not found")
}
func (s *userService) Create(user models.User) (*models.User, error) {
// In a real app, you'd validate input, hash passwords, etc.
newUser := models.User{
ID: uint(nextID),
Username: user.Username,
Email: user.Email,
FullName: user.FullName,
// Hash password before saving
}
users[nextID] = &newUser
nextID++
return &newUser, nil
}
func (s *userService) Update(user models.User) (*models.User, error) {
if _, exists := users[int(user.ID)]; !exists {
return nil, errors.New("user not found")
}
users[int(user.ID)] = &user
return &user, nil
}
func (s *userService) Delete(id int) error {
if _, exists := users[id]; !exists {
return errors.New("user not found")
}
delete(users, id)
return nil
}
utils/
The utils/
directory contains helper functions and utilities used throughout the application.
Example utils/jwt.go
:
package utils
import (
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v4"
)
// TokenClaims represents the claims in a JWT token
type TokenClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken creates a new JWT token for a user
func GenerateToken(userID uint, username string) (string, error) {
// Get secret key from environment
secretKey := os.Getenv("JWT_SECRET_KEY")
if secretKey == "" {
secretKey = "default_secret_key" // Not for production!
}
claims := TokenClaims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Token expires in 24 hours
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secretKey))
return tokenString, err
}
// ValidateToken validates a JWT token and returns its claims
func ValidateToken(tokenString string) (*TokenClaims, error) {
// Get secret key from environment
secretKey := os.Getenv("JWT_SECRET_KEY")
if secretKey == "" {
secretKey = "default_secret_key" // Not for production!
}
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
static/
This directory stores static files like CSS, JavaScript, images, etc. In a typical Gin application, you'd serve these files using Gin's static file middleware:
// In your main.go or routes setup
router.Static("/static", "./static")
Advanced Directory Structure
As your application grows, you might want to extend this structure with additional directories:
my-gin-app/
├── main.go
├── go.mod
├── go.sum
├── config/
├── controllers/
├── middlewares/
├── models/
├── repositories/ # Data access layer
├── routes/
├── services/
├── utils/
├── static/
├── templates/ # HTML templates
├── docs/ # API documentation
└── tests/ # Test files
Additional Directories
repositories/
: Provides data access abstraction, hiding database implementation details from servicestemplates/
: Stores HTML templates for server-side renderingdocs/
: Contains API documentation, often generated with Swaggertests/
: Houses unit, integration, and end-to-end tests
Best Practices for Gin Directory Structure
-
Keep the Package Structure Flat: Avoid deep nesting of packages
-
Use Interfaces for Dependency Injection: Interfaces help with testing and flexibility
-
Group Related Functionality: Co-locate files that work together
-
Separate Domain Logic from Infrastructure: Business rules should be independent of frameworks
-
Consistent Naming: Use consistent naming conventions throughout your project
-
Avoid Circular Dependencies: Structure your code to prevent import cycles
Practical Example: Blog API
Let's see how this directory structure applies to a simple blog API:
blog-api/
├── main.go
├── config/
│ └── config.go
├── controllers/
│ ├── post_controller.go
│ └── user_controller.go
├── middlewares/
│ ├── auth.go
│ └── logging.go
├── models/
│ ├── post.go
│ └── user.go
├── services/
│ ├── post_service.go
│ └── user_service.go
├ ── repositories/
│ ├── post_repository.go
│ └── user_repository.go
├── routes/
│ └── routes.go
└── utils/
├── jwt.go
└── password.go
This structure clearly separates concerns and makes it easy to navigate the codebase.
Summary
A well-organized directory structure is essential for developing maintainable Gin applications. The structure we've explored:
- Separates concerns with dedicated directories for models, controllers, services, etc.
- Enables modularity and testability
- Follows Go best practices
- Scales well as your application grows
Remember that directory structure should serve your application's needs. While following conventions is helpful, adapt the structure to fit your specific requirements.
Additional Resources
- Gin Framework Official Documentation
- Standard Go Project Layout
- Clean Architecture in Go
- Effective Go
Exercises
- Set up a basic Gin project with the directory structure described in this guide.
- Create a simple REST API for a resource of your choice (e.g., products, books, articles).
- Implement authentication middleware to protect specific routes.
- Add input validation to your controllers using Gin's binding capabilities.
- Refactor an existing Gin application to follow this directory structure.
By following these patterns, you'll build Gin applications that are easier to maintain and scale over time.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!