Gin Framework Guide¶
Applies to: Gin 1.9+, REST APIs, Microservices, Web Applications Language Guide: @.claude/skills/go-guide/SKILL.md
Overview¶
Gin is a high-performance HTTP web framework written in Go featuring a martini-like API with performance up to 40x faster. It's the most popular Go web framework, ideal for building REST APIs and microservices.
Use Gin when: - Building high-performance REST APIs - You need a mature, well-documented framework - Middleware ecosystem is important - You want a balanced approach (not too minimal, not too heavy)
Consider alternatives when: - You need maximum minimalism (use standard library) - You want built-in WebSocket support (use Fiber) - You prefer a different API style (use Echo)
Project Structure¶
myproject/
├── cmd/
│ └── api/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration
│ ├── handler/
│ │ ├── handler.go # Handler interface
│ │ ├── user.go # User handlers
│ │ └── auth.go # Auth handlers
│ ├── middleware/
│ │ ├── auth.go # JWT middleware
│ │ ├── cors.go # CORS middleware
│ │ └── logger.go # Request logging
│ ├── model/
│ │ ├── user.go # User model
│ │ └── response.go # Response models
│ ├── repository/
│ │ ├── repository.go # Repository interface
│ │ └── user.go # User repository
│ ├── service/
│ │ ├── service.go # Service interface
│ │ └── user.go # User service
│ └── router/
│ └── router.go # Route definitions
├── pkg/
│ ├── validator/
│ │ └── validator.go # Custom validators
│ └── response/
│ └── response.go # Response helpers
├── migrations/
├── .env.example
├── go.mod
├── go.sum
├── Makefile
└── README.md
Application Setup¶
cmd/api/main.go¶
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"myproject/internal/config"
"myproject/internal/handler"
"myproject/internal/middleware"
"myproject/internal/repository"
"myproject/internal/router"
"myproject/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Set Gin mode
if cfg.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Initialize database
db, err := initDB(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Initialize layers
repos := repository.NewRepositories(db)
services := service.NewServices(repos, cfg)
handlers := handler.NewHandlers(services)
// Setup router
r := router.Setup(handlers, middleware.NewMiddleware(cfg))
// Create server
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
go func() {
log.Printf("Server starting on port %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
func initDB(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
Configuration¶
internal/config/config.go¶
package config
import (
"os"
"time"
"github.com/joho/godotenv"
)
type Config struct {
Environment string
Port string
DatabaseURL string
JWTSecret string
JWTAccessDuration time.Duration
JWTRefreshDuration time.Duration
CORSOrigins []string
}
func Load() (*Config, error) {
// Load .env file in development
_ = godotenv.Load()
return &Config{
Environment: getEnv("ENVIRONMENT", "development"),
Port: getEnv("PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/myapp?sslmode=disable"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
JWTAccessDuration: parseDuration(getEnv("JWT_ACCESS_DURATION", "15m")),
JWTRefreshDuration: parseDuration(getEnv("JWT_REFRESH_DURATION", "168h")),
CORSOrigins: []string{getEnv("CORS_ORIGINS", "*")},
}, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func parseDuration(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
return 15 * time.Minute
}
return d
}
Models¶
internal/model/user.go¶
package model
import (
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
IsActive bool `json:"is_active" gorm:"default:true"`
IsAdmin bool `json:"is_admin" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (u *User) SetPassword(password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hash)
return nil
}
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
func (u *User) FullName() string {
return u.FirstName + " " + u.LastName
}
// Request DTOs
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
FirstName string `json:"first_name" binding:"required,min=1,max=100"`
LastName string `json:"last_name" binding:"required,min=1,max=100"`
}
type UpdateUserRequest struct {
FirstName *string `json:"first_name" binding:"omitempty,min=1,max=100"`
LastName *string `json:"last_name" binding:"omitempty,min=1,max=100"`
IsActive *bool `json:"is_active"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
// Response DTOs
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
FullName string `json:"full_name"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (u *User) ToResponse() *UserResponse {
return &UserResponse{
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
FullName: u.FullName(),
IsActive: u.IsActive,
IsAdmin: u.IsAdmin,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
internal/model/response.go¶
package model
type Response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type PaginatedResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Meta *PageMeta `json:"meta"`
}
type PageMeta struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
}
func NewSuccessResponse(data interface{}) *Response {
return &Response{
Success: true,
Data: data,
}
}
func NewErrorResponse(message string) *Response {
return &Response{
Success: false,
Error: message,
}
}
func NewPaginatedResponse(data interface{}, page, perPage int, total int64) *PaginatedResponse {
totalPages := int(total) / perPage
if int(total)%perPage > 0 {
totalPages++
}
return &PaginatedResponse{
Success: true,
Data: data,
Meta: &PageMeta{
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
},
}
}
Repository Layer¶
internal/repository/repository.go¶
package repository
import "gorm.io/gorm"
type Repositories struct {
User UserRepository
}
func NewRepositories(db *gorm.DB) *Repositories {
return &Repositories{
User: NewUserRepository(db),
}
}
internal/repository/user.go¶
package repository
import (
"context"
"myproject/internal/model"
"gorm.io/gorm"
)
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
GetByID(ctx context.Context, id uint) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
GetAll(ctx context.Context, page, perPage int) ([]model.User, int64, error)
Update(ctx context.Context, user *model.User) error
Delete(ctx context.Context, id uint) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepository) GetByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) GetAll(ctx context.Context, page, perPage int) ([]model.User, int64, error) {
var users []model.User
var total int64
offset := (page - 1) * perPage
err := r.db.WithContext(ctx).Model(&model.User{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
err = r.db.WithContext(ctx).
Offset(offset).
Limit(perPage).
Order("created_at DESC").
Find(&users).Error
if err != nil {
return nil, 0, err
}
return users, total, nil
}
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.User{}, id).Error
}
Service Layer¶
internal/service/service.go¶
package service
import (
"myproject/internal/config"
"myproject/internal/repository"
)
type Services struct {
User UserService
Auth AuthService
}
func NewServices(repos *repository.Repositories, cfg *config.Config) *Services {
authService := NewAuthService(repos.User, cfg)
return &Services{
User: NewUserService(repos.User),
Auth: authService,
}
}
internal/service/user.go¶
package service
import (
"context"
"errors"
"myproject/internal/model"
"myproject/internal/repository"
"gorm.io/gorm"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
ErrInvalidCredentials = errors.New("invalid credentials")
)
type UserService interface {
Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error)
GetByID(ctx context.Context, id uint) (*model.User, error)
GetAll(ctx context.Context, page, perPage int) ([]model.User, int64, error)
Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error)
Delete(ctx context.Context, id uint) error
}
type userService struct {
userRepo repository.UserRepository
}
func NewUserService(userRepo repository.UserRepository) UserService {
return &userService{userRepo: userRepo}
}
func (s *userService) Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
// Check if user exists
existing, err := s.userRepo.GetByEmail(ctx, req.Email)
if err == nil && existing != nil {
return nil, ErrUserAlreadyExists
}
user := &model.User{
Email: req.Email,
FirstName: req.FirstName,
LastName: req.LastName,
}
if err := user.SetPassword(req.Password); err != nil {
return nil, err
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id uint) (*model.User, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return user, nil
}
func (s *userService) GetAll(ctx context.Context, page, perPage int) ([]model.User, int64, error) {
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 100 {
perPage = 20
}
return s.userRepo.GetAll(ctx, page, perPage)
}
func (s *userService) Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error) {
user, err := s.GetByID(ctx, id)
if err != nil {
return nil, err
}
if req.FirstName != nil {
user.FirstName = *req.FirstName
}
if req.LastName != nil {
user.LastName = *req.LastName
}
if req.IsActive != nil {
user.IsActive = *req.IsActive
}
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) Delete(ctx context.Context, id uint) error {
_, err := s.GetByID(ctx, id)
if err != nil {
return err
}
return s.userRepo.Delete(ctx, id)
}
internal/service/auth.go¶
package service
import (
"context"
"errors"
"time"
"myproject/internal/config"
"myproject/internal/model"
"myproject/internal/repository"
"github.com/golang-jwt/jwt/v5"
)
type AuthService interface {
Login(ctx context.Context, req *model.LoginRequest) (*model.TokenResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*model.TokenResponse, error)
ValidateToken(tokenString string) (*Claims, error)
}
type Claims struct {
UserID uint `json:"user_id"`
IsAdmin bool `json:"is_admin"`
jwt.RegisteredClaims
}
type authService struct {
userRepo repository.UserRepository
cfg *config.Config
}
func NewAuthService(userRepo repository.UserRepository, cfg *config.Config) AuthService {
return &authService{
userRepo: userRepo,
cfg: cfg,
}
}
func (s *authService) Login(ctx context.Context, req *model.LoginRequest) (*model.TokenResponse, error) {
user, err := s.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, ErrInvalidCredentials
}
if !user.CheckPassword(req.Password) {
return nil, ErrInvalidCredentials
}
if !user.IsActive {
return nil, errors.New("account is deactivated")
}
return s.generateTokens(user)
}
func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*model.TokenResponse, error) {
claims, err := s.ValidateToken(refreshToken)
if err != nil {
return nil, err
}
user, err := s.userRepo.GetByID(ctx, claims.UserID)
if err != nil {
return nil, ErrUserNotFound
}
if !user.IsActive {
return nil, errors.New("account is deactivated")
}
return s.generateTokens(user)
}
func (s *authService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.cfg.JWTSecret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
func (s *authService) generateTokens(user *model.User) (*model.TokenResponse, error) {
now := time.Now()
accessExp := now.Add(s.cfg.JWTAccessDuration)
refreshExp := now.Add(s.cfg.JWTRefreshDuration)
// Access token
accessClaims := &Claims{
UserID: user.ID,
IsAdmin: user.IsAdmin,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(accessExp),
IssuedAt: jwt.NewNumericDate(now),
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString([]byte(s.cfg.JWTSecret))
if err != nil {
return nil, err
}
// Refresh token
refreshClaims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExp),
IssuedAt: jwt.NewNumericDate(now),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString([]byte(s.cfg.JWTSecret))
if err != nil {
return nil, err
}
return &model.TokenResponse{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
TokenType: "Bearer",
ExpiresIn: int64(s.cfg.JWTAccessDuration.Seconds()),
}, nil
}
Handlers¶
internal/handler/handler.go¶
package handler
import "myproject/internal/service"
type Handlers struct {
User *UserHandler
Auth *AuthHandler
}
func NewHandlers(services *service.Services) *Handlers {
return &Handlers{
User: NewUserHandler(services.User),
Auth: NewAuthHandler(services.Auth),
}
}
internal/handler/user.go¶
package handler
import (
"errors"
"net/http"
"strconv"
"myproject/internal/model"
"myproject/internal/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userService service.UserService
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// GetUsers godoc
// @Summary Get all users
// @Tags users
// @Accept json
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param per_page query int false "Items per page" default(20)
// @Success 200 {object} model.PaginatedResponse
// @Router /users [get]
func (h *UserHandler) GetUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
users, total, err := h.userService.GetAll(c.Request.Context(), page, perPage)
if err != nil {
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
return
}
responses := make([]*model.UserResponse, len(users))
for i, user := range users {
responses[i] = user.ToResponse()
}
c.JSON(http.StatusOK, model.NewPaginatedResponse(responses, page, perPage, total))
}
// GetUser godoc
// @Summary Get user by ID
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} model.Response
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse("invalid user ID"))
return
}
user, err := h.userService.GetByID(c.Request.Context(), uint(id))
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
c.JSON(http.StatusNotFound, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, model.NewSuccessResponse(user.ToResponse()))
}
// CreateUser godoc
// @Summary Create a new user
// @Tags users
// @Accept json
// @Produce json
// @Param request body model.CreateUserRequest true "User data"
// @Success 201 {object} model.Response
// @Router /users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req model.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(err.Error()))
return
}
user, err := h.userService.Create(c.Request.Context(), &req)
if err != nil {
if errors.Is(err, service.ErrUserAlreadyExists) {
c.JSON(http.StatusConflict, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusCreated, model.NewSuccessResponse(user.ToResponse()))
}
// UpdateUser godoc
// @Summary Update user
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param request body model.UpdateUserRequest true "User data"
// @Success 200 {object} model.Response
// @Router /users/{id} [patch]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse("invalid user ID"))
return
}
var req model.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(err.Error()))
return
}
user, err := h.userService.Update(c.Request.Context(), uint(id), &req)
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
c.JSON(http.StatusNotFound, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, model.NewSuccessResponse(user.ToResponse()))
}
// DeleteUser godoc
// @Summary Delete user
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 204
// @Router /users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse("invalid user ID"))
return
}
err = h.userService.Delete(c.Request.Context(), uint(id))
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
c.JSON(http.StatusNotFound, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusInternalServerError, model.NewErrorResponse(err.Error()))
return
}
c.Status(http.StatusNoContent)
}
// GetCurrentUser godoc
// @Summary Get current authenticated user
// @Tags users
// @Accept json
// @Produce json
// @Success 200 {object} model.Response
// @Router /users/me [get]
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse("unauthorized"))
return
}
user, err := h.userService.GetByID(c.Request.Context(), userID.(uint))
if err != nil {
c.JSON(http.StatusNotFound, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, model.NewSuccessResponse(user.ToResponse()))
}
internal/handler/auth.go¶
package handler
import (
"net/http"
"myproject/internal/model"
"myproject/internal/service"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
authService service.AuthService
}
func NewAuthHandler(authService service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
// Login godoc
// @Summary Login user
// @Tags auth
// @Accept json
// @Produce json
// @Param request body model.LoginRequest true "Login credentials"
// @Success 200 {object} model.TokenResponse
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req model.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(err.Error()))
return
}
tokens, err := h.authService.Login(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, tokens)
}
// Refresh godoc
// @Summary Refresh access token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body map[string]string true "Refresh token"
// @Success 200 {object} model.TokenResponse
// @Router /auth/refresh [post]
func (h *AuthHandler) Refresh(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(err.Error()))
return
}
tokens, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, tokens)
}
Middleware¶
internal/middleware/auth.go¶
package middleware
import (
"net/http"
"strings"
"myproject/internal/model"
"myproject/internal/service"
"github.com/gin-gonic/gin"
)
func (m *Middleware) Auth() gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if header == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, model.NewErrorResponse("missing authorization header"))
return
}
parts := strings.Split(header, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, model.NewErrorResponse("invalid authorization header"))
return
}
claims, err := m.authService.ValidateToken(parts[1])
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, model.NewErrorResponse("invalid token"))
return
}
c.Set("user_id", claims.UserID)
c.Set("is_admin", claims.IsAdmin)
c.Next()
}
}
func (m *Middleware) AdminOnly() gin.HandlerFunc {
return func(c *gin.Context) {
isAdmin, exists := c.Get("is_admin")
if !exists || !isAdmin.(bool) {
c.AbortWithStatusJSON(http.StatusForbidden, model.NewErrorResponse("admin access required"))
return
}
c.Next()
}
}
type Middleware struct {
authService service.AuthService
}
func NewMiddleware(cfg interface{}) *Middleware {
return &Middleware{}
}
func (m *Middleware) SetAuthService(authService service.AuthService) {
m.authService = authService
}
internal/middleware/logger.go¶
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
clientIP := c.ClientIP()
method := c.Request.Method
if query != "" {
path = path + "?" + query
}
log.Printf("[GIN] %3d | %13v | %15s | %-7s %s",
status,
latency,
clientIP,
method,
path,
)
}
}
Router¶
internal/router/router.go¶
package router
import (
"myproject/internal/handler"
"myproject/internal/middleware"
"github.com/gin-gonic/gin"
)
func Setup(handlers *handler.Handlers, mw *middleware.Middleware) *gin.Engine {
r := gin.New()
// Global middleware
r.Use(gin.Recovery())
r.Use(middleware.Logger())
r.Use(middleware.CORS())
// Health check
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// API v1 routes
v1 := r.Group("/api/v1")
{
// Auth routes (public)
auth := v1.Group("/auth")
{
auth.POST("/login", handlers.Auth.Login)
auth.POST("/refresh", handlers.Auth.Refresh)
}
// User routes
users := v1.Group("/users")
{
// Public
users.POST("", handlers.User.CreateUser)
// Protected
users.Use(mw.Auth())
users.GET("", handlers.User.GetUsers)
users.GET("/me", handlers.User.GetCurrentUser)
users.GET("/:id", handlers.User.GetUser)
users.PATCH("/:id", handlers.User.UpdateUser)
// Admin only
users.DELETE("/:id", mw.AdminOnly(), handlers.User.DeleteUser)
}
}
return r
}
Testing¶
internal/handler/user_test.go¶
package handler_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"myproject/internal/handler"
"myproject/internal/model"
"myproject/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockUserService is a mock implementation of UserService
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) Create(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
args := m.Called(ctx, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) GetByID(ctx context.Context, id uint) (*model.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) GetAll(ctx context.Context, page, perPage int) ([]model.User, int64, error) {
args := m.Called(ctx, page, perPage)
return args.Get(0).([]model.User), args.Get(1).(int64), args.Error(2)
}
func (m *MockUserService) Update(ctx context.Context, id uint, req *model.UpdateUserRequest) (*model.User, error) {
args := m.Called(ctx, id, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserService) Delete(ctx context.Context, id uint) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func setupTestRouter(h *handler.UserHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/users", h.CreateUser)
r.GET("/users/:id", h.GetUser)
r.GET("/users", h.GetUsers)
return r
}
func TestCreateUser(t *testing.T) {
mockService := new(MockUserService)
h := handler.NewUserHandler(mockService)
router := setupTestRouter(h)
t.Run("successful creation", func(t *testing.T) {
expectedUser := &model.User{
ID: 1,
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
}
mockService.On("Create", mock.Anything, mock.Anything).Return(expectedUser, nil).Once()
body := model.CreateUserRequest{
Email: "test@example.com",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
mockService.AssertExpectations(t)
})
t.Run("duplicate email", func(t *testing.T) {
mockService.On("Create", mock.Anything, mock.Anything).
Return(nil, service.ErrUserAlreadyExists).Once()
body := model.CreateUserRequest{
Email: "existing@example.com",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusConflict, w.Code)
mockService.AssertExpectations(t)
})
}
func TestGetUser(t *testing.T) {
mockService := new(MockUserService)
h := handler.NewUserHandler(mockService)
router := setupTestRouter(h)
t.Run("user found", func(t *testing.T) {
expectedUser := &model.User{
ID: 1,
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
}
mockService.On("GetByID", mock.Anything, uint(1)).Return(expectedUser, nil).Once()
req, _ := http.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
mockService.AssertExpectations(t)
})
t.Run("user not found", func(t *testing.T) {
mockService.On("GetByID", mock.Anything, uint(999)).
Return(nil, service.ErrUserNotFound).Once()
req, _ := http.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
mockService.AssertExpectations(t)
})
}
Commands Reference¶
# Initialize project
go mod init myproject
# Install dependencies
go mod tidy
# Run development server
go run cmd/api/main.go
# Build binary
go build -o bin/api cmd/api/main.go
# Run tests
go test ./...
go test -v -cover ./...
# Run with race detection
go test -race ./...
# Lint
golangci-lint run
# Generate Swagger docs
swag init -g cmd/api/main.go
# Database migrations (using golang-migrate)
migrate -path migrations -database "$DATABASE_URL" up
migrate -path migrations -database "$DATABASE_URL" down
Dependencies¶
// go.mod
module myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.14.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/stretchr/testify v1.8.4 // testing
github.com/swaggo/gin-swagger v1.6.0 // swagger
github.com/swaggo/swag v1.16.2 // swagger
)
Best Practices¶
Gin-Specific Guidelines¶
- ✓ Use application factory pattern for testability
- ✓ Group routes with versioning (/api/v1)
- ✓ Use middleware for cross-cutting concerns
- ✓ Use gin.Context for request-scoped data
- ✓ Use binding tags for validation
- ✓ Return consistent JSON response structure
- ✓ Use proper HTTP status codes
Performance Guidelines¶
- ✓ Use gin.ReleaseMode in production
- ✓ Configure proper timeouts
- ✓ Use connection pooling for database
- ✓ Implement graceful shutdown
- ✓ Use pagination for list endpoints