Skip to main content

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:

  1. Initializes the Gin router
  2. Sets up configuration
  3. Registers routes
  4. Starts the HTTP server

Here's a simple example of a main.go file:

go
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 dependencies
  • go.sum: Contains checksums for dependency verification

Here's a sample go.mod file:

go
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:

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:

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:

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:

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:

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:

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:

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:

go
// 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 services
  • templates/: Stores HTML templates for server-side rendering
  • docs/: Contains API documentation, often generated with Swagger
  • tests/: Houses unit, integration, and end-to-end tests

Best Practices for Gin Directory Structure

  1. Keep the Package Structure Flat: Avoid deep nesting of packages

  2. Use Interfaces for Dependency Injection: Interfaces help with testing and flexibility

  3. Group Related Functionality: Co-locate files that work together

  4. Separate Domain Logic from Infrastructure: Business rules should be independent of frameworks

  5. Consistent Naming: Use consistent naming conventions throughout your project

  6. 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

Exercises

  1. Set up a basic Gin project with the directory structure described in this guide.
  2. Create a simple REST API for a resource of your choice (e.g., products, books, articles).
  3. Implement authentication middleware to protect specific routes.
  4. Add input validation to your controllers using Gin's binding capabilities.
  5. 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.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)