RESTful API design patterns, Gin framework best practices, error handling, validation, and response formatting for platform-go
This skill provides guidelines for designing and implementing RESTful APIs using Gin framework.
Apply this skill when:
This skill includes ready-to-use Swagger/OpenAPI scripts:
# Generate Swagger documentation from code comments
bash .github/skills/api-design-patterns/scripts/swagger-generate.sh
# Validate Swagger specification
bash .github/skills/api-design-patterns/scripts/swagger-validate.sh
All API endpoints must have complete Swagger documentation following OpenAPI 3.0 standard.
// Install swag for Go
// go get -u github.com/swaggo/swag/cmd/swag
// go get -u github.com/swaggo/gin-swagger
// go get -u github.com/swaggo/files
// Generate Swagger docs
// swag init -g cmd/api/main.go
// main.go
package main
import (
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginswagger "github.com/swaggo/gin-swagger"
_ "github.com/linskybing/platform-go/docs" // Generated docs
)
// @title Platform-Go API
// @version 1.0
// @description REST API for platform-go project
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email [email protected]
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host api.example.com
// @basePath /api/v1
// @schemes https
func main() {
r := gin.Default()
// Swagger endpoint
r.GET("/swagger/*any", ginswagger.WrapHandler(swaggerfiles.Handler))
r.Run()
}
Every handler must have complete Swagger annotations:
// internal/api/handler/user_handler.go
package handler
import (
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
)
// CreateUser godoc
// @Summary Create a new user
// @Description Create a new user with username and email
// @Tags users
// @Accept json
// @Produce json
// @Param request body CreateUserRequest true "Create user request"
// @Success 201 {object} UserResponse
// @Failure 400 {object} ErrorResponse "Invalid input"
// @Failure 409 {object} ErrorResponse "User already exists"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /users [post]
// @Security ApiKeyAuth
func (h *UserHandler) Create(c *gin.Context) {
var req dto.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input"})
return
}
user, err := h.userService.CreateUser(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, dto.ToUserResponse(user))
}
// GetUser godoc
// @Summary Get user by ID
// @Description Retrieve a user by their ID
// @Tags users
// @Produce json
// @Param id path uint true "User ID"
// @Success 200 {object} UserResponse
// @Failure 404 {object} ErrorResponse "User not found"
// @Router /users/{id} [get]
// @Security ApiKeyAuth
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
user, err := h.userService.GetUser(c.Request.Context(), uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, dto.ToUserResponse(user))
}
// ListUsers godoc
// @Summary List all users
// @Description List users with pagination and filtering
// @Tags users
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(20)
// @Param role query string false "Filter by role"
// @Param status query string false "Filter by status"
// @Success 200 {object} ListUsersResponse
// @Failure 500 {object} ErrorResponse
// @Router /users [get]
// @Security ApiKeyAuth
func (h *UserHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
users, total, err := h.userService.ListUsers(c.Request.Context(), &ListFilter{
Page: page,
Limit: limit,
Role: c.Query("role"),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
},
})
}
// UpdateUser godoc
// @Summary Update user
// @Description Update user information
// @Tags users
// @Accept json
// @Produce json
// @Param id path uint true "User ID"
// @Param request body UpdateUserRequest true "Update request"
// @Success 200 {object} UserResponse
// @Failure 400 {object} ErrorResponse "Invalid input"
// @Failure 404 {object} ErrorResponse "User not found"
// @Router /users/{id} [put]
// @Security ApiKeyAuth
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req dto.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input"})
return
}
user, err := h.userService.UpdateUser(c.Request.Context(), uint(id), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dto.ToUserResponse(user))
}
// DeleteUser godoc
// @Summary Delete user
// @Description Delete a user by ID
// @Tags users
// @Produce json
// @Param id path uint true "User ID"
// @Success 200 {object} SuccessResponse
// @Failure 404 {object} ErrorResponse "User not found"
// @Router /users/{id} [delete]
// @Security ApiKeyAuth
func (h *UserHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.userService.DeleteUser(c.Request.Context(), uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}
// internal/api/handler/dto/request.go
// CreateUserRequest represents user creation request
// @Description User creation request with validation
type CreateUserRequest struct {
// User username (3-32 alphanumeric characters)
// Required: true
// Example: john_doe
Username string `json:"username" binding:"required,min=3,max=32,alphanum" example:"john_doe"`
// User email address
// Required: true
// Format: email
// Example: [email protected]
Email string `json:"email" binding:"required,email" example:"[email protected]"`
// User password (minimum 8 characters)
// Required: true
// MinLength: 8
// Example: SecurePass123!
Password string `json:"password" binding:"required,min=8" example:"SecurePass123!"`
// User role (admin, manager, user)
// Optional
// Enum: [admin, manager, user]
// Default: user
Role string `json:"role" binding:"omitempty,oneof=admin manager user" example:"user"`
}
// UpdateUserRequest represents user update request
type UpdateUserRequest struct {
// Updated username
// Optional
// Example: john_doe_updated
Username string `json:"username,omitempty" example:"john_doe_updated"`
// Updated email
// Optional
// Format: email
Email string `json:"email,omitempty" example:"[email protected]"`
// Updated password
// Optional
Password string `json:"password,omitempty"`
// Updated role
// Optional
// Enum: [admin, manager, user]
Role string `json:"role,omitempty" example:"manager"`
}
// internal/api/handler/dto/response.go
// UserResponse represents user data in responses
// @Description User response model with all public fields
type UserResponse struct {
// User unique identifier
// Example: 123
ID uint `json:"id" example:"123"`
// User username
// Example: john_doe
Username string `json:"username" example:"john_doe"`
// User email address
// Example: [email protected]
Email string `json:"email" example:"[email protected]"`
// User role
// Example: user
Role string `json:"role" example:"user"`
// User creation timestamp
// Format: date-time
// Example: 2026-02-02T10:00:00Z
CreatedAt time.Time `json:"created_at" example:"2026-02-02T10:00:00Z"`
// User last update timestamp
// Format: date-time
// Example: 2026-02-02T10:00:00Z
UpdatedAt time.Time `json:"updated_at" example:"2026-02-02T10:00:00Z"`
}
// ErrorResponse represents error response
// @Description Standard error response format
type ErrorResponse struct {
// HTTP status code
// Example: 400
Code int `json:"code" example:"400"`
// Error message
// Example: Invalid input
Error string `json:"error" example:"Invalid input"`
// Additional error details
// Example: {"field":"username","message":"minimum length is 3"}
Details map[string]interface{} `json:"details,omitempty"`
}
// SuccessResponse represents success response
type SuccessResponse struct {
// HTTP status code
// Example: 200
Code int `json:"code" example:"200"`
// Success message
// Example: Operation completed successfully
Message string `json:"message" example:"Operation completed successfully"`
// Response data
Data interface{} `json:"data,omitempty"`
}
// ListUsersResponse represents paginated users response
type ListUsersResponse struct {
Data []UserResponse `json:"data"`
Pagination struct {
Page int `json:"page" example:"1"`
Limit int `json:"limit" example:"20"`
Total int64 `json:"total" example:"100"`
TotalPages int `json:"total_pages" example:"5"`
} `json:"pagination"`
}
// internal/api/routes.go
package api
import (
"github.com/gin-gonic/gin"
"github.com/linskybing/platform-go/internal/api/handler"
)
func RegisterRoutes(r *gin.Engine, userHandler *handler.UserHandler) {
// API v1 routes
v1 := r.Group("/api/v1")
// User endpoints
users := v1.Group("/users")
{
// Create user (POST /api/v1/users)
users.POST("", userHandler.Create)
// List users (GET /api/v1/users)
users.GET("", userHandler.List)
// Get user (GET /api/v1/users/:id)
users.GET("/:id", userHandler.Get)
// Update user (PUT /api/v1/users/:id)
users.PUT("/:id", userHandler.Update)
// Delete user (DELETE /api/v1/users/:id)
users.DELETE("/:id", userHandler.Delete)
}
}
// main.go
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @description JWT token with Bearer prefix
// Example: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// @securityDefinitions.oauth2 OAuth2
// @flow implicit
// @authorizationUrl https://example.com/oauth/authorize
// @tokenUrl https://example.com/oauth/token
// @scopes.read Read access to protected resources
// @scopes.write Write access to protected resources
// Middleware to handle Bearer token
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, ErrorResponse{
Code: http.StatusUnauthorized,
Error: "missing authorization token",
})
c.Abort()
return
}
// Validate token
if !strings.HasPrefix(token, "Bearer ") {
c.JSON(http.StatusUnauthorized, ErrorResponse{
Code: http.StatusUnauthorized,
Error: "invalid token format",
})
c.Abort()
return
}
c.Next()
}
}
# Makefile
.PHONY: swagger
# Generate Swagger documentation