WHEN: Go API review with Gin/Echo/Fiber/Chi, router patterns, middleware, request handling WHAT: Router organization + Middleware patterns + Request validation + Error responses + OpenAPI WHEN NOT: General Go → go-reviewer, Rust API → rust-api-reviewer
Install
npx skillscat add physics91/claude-vibe/go-api-reviewer Install via the SkillsCat registry.
SKILL.md
Go API Reviewer Skill
Purpose
Reviews Go API projects using Gin, Echo, Fiber, or Chi for routing, middleware, and API patterns.
When to Use
- Go REST API code review
- Gin/Echo/Fiber/Chi project review
- Middleware implementation review
- API request/response handling
- API documentation review
Project Detection
github.com/gin-gonic/ginimportgithub.com/labstack/echoimportgithub.com/gofiber/fiberimportgithub.com/go-chi/chiimporthandlers/,routes/,middleware/directories
Workflow
Step 1: Analyze Project
**Framework**: Gin v1.9+
**Router**: Group-based routing
**Middleware**: Auth, CORS, Logger, Recovery
**Validation**: go-playground/validator
**Docs**: Swagger/OpenAPIStep 2: Select Review Areas
AskUserQuestion:
"Which areas to review?"
Options:
- Full API review (recommended)
- Router and handler patterns
- Middleware implementation
- Request validation
- Error handling and responses
multiSelect: trueDetection Rules
Router Organization
| Check | Recommendation | Severity |
|---|---|---|
| All routes in main | Use router groups | MEDIUM |
| No versioning | Add /api/v1 prefix | MEDIUM |
| Inconsistent naming | Follow REST conventions | LOW |
| No route grouping | Group by resource | MEDIUM |
// BAD: All routes in main.go
func main() {
r := gin.Default()
r.GET("/users", getUsers)
r.POST("/users", createUser)
r.GET("/users/:id", getUser)
r.GET("/products", getProducts)
// ... 50 more routes
}
// GOOD: Organized route groups (Gin)
func SetupRouter() *gin.Engine {
r := gin.Default()
api := r.Group("/api/v1")
{
users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
products := api.Group("/products")
{
products.GET("", listProducts)
products.GET("/:id", getProduct)
}
}
return r
}
// GOOD: Separate route files
// routes/users.go
func RegisterUserRoutes(rg *gin.RouterGroup) {
users := rg.Group("/users")
h := NewUserHandler()
users.GET("", h.List)
users.POST("", h.Create)
users.GET("/:id", h.Get)
}Middleware Patterns
| Check | Recommendation | Severity |
|---|---|---|
| Auth in handler | Extract to middleware | HIGH |
| No recovery middleware | Add panic recovery | HIGH |
| No request ID | Add request ID middleware | MEDIUM |
| Middleware order wrong | Order: Logger → Recovery → Auth | MEDIUM |
// GOOD: Middleware stack (Gin)
func SetupMiddleware(r *gin.Engine) {
// Order matters!
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(RequestIDMiddleware())
r.Use(CORSMiddleware())
}
// GOOD: Auth middleware
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
claims, err := ValidateToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
c.Set("user_id", claims.UserID)
c.Next()
}
}
// Usage
api := r.Group("/api/v1")
api.Use(AuthMiddleware())
// GOOD: Request ID middleware
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}Request Validation
| Check | Recommendation | Severity |
|---|---|---|
| Manual validation | Use validator tags | MEDIUM |
| No binding errors | Return validation errors | HIGH |
| No request DTOs | Define request structs | MEDIUM |
| Missing required fields | Add binding:"required" | HIGH |
// GOOD: Request struct with validation
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=150"`
Role string `json:"role" binding:"oneof=admin user guest"`
Password string `json:"password" binding:"required,min=8"`
}
// GOOD: Handler with validation
func (h *UserHandler) Create(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{
"error": "validation_error",
"details": formatValidationErrors(err),
})
return
}
user, err := h.service.Create(c.Request.Context(), &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(201, user)
}
// GOOD: Format validation errors
func formatValidationErrors(err error) map[string]string {
errors := make(map[string]string)
var ve validator.ValidationErrors
if errors.As(err, &ve) {
for _, e := range ve {
field := strings.ToLower(e.Field())
errors[field] = getErrorMessage(e)
}
}
return errors
}Error Handling
| Check | Recommendation | Severity |
|---|---|---|
| Inconsistent error format | Use standard error response | HIGH |
| Internal errors exposed | Hide implementation details | HIGH |
| No error codes | Add error codes | MEDIUM |
| HTTP status inconsistent | Follow REST conventions | MEDIUM |
// GOOD: Standard error response
type ErrorResponse struct {
Error string `json:"error"`
Code string `json:"code,omitempty"`
Details map[string]string `json:"details,omitempty"`
}
// GOOD: Custom errors
var (
ErrNotFound = &AppError{Code: "NOT_FOUND", Status: 404}
ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Status: 401}
ErrConflict = &AppError{Code: "CONFLICT", Status: 409}
)
type AppError struct {
Code string
Status int
Message string
}
func (e *AppError) Error() string {
return e.Message
}
// GOOD: Error handler
func handleError(c *gin.Context, err error) {
var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(appErr.Status, ErrorResponse{
Error: appErr.Message,
Code: appErr.Code,
})
return
}
// Log internal error, return generic message
log.Printf("internal error: %v", err)
c.JSON(500, ErrorResponse{
Error: "Internal server error",
Code: "INTERNAL_ERROR",
})
}Framework-Specific (Echo)
// Echo example
func SetupEcho() *echo.Echo {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.Use(middleware.CORS())
api := e.Group("/api/v1")
api.Use(AuthMiddleware)
RegisterUserRoutes(api)
return e
}
// Echo handler
func (h *UserHandler) Create(c echo.Context) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(400, "invalid request")
}
if err := c.Validate(&req); err != nil {
return err
}
user, err := h.service.Create(c.Request().Context(), &req)
if err != nil {
return err
}
return c.JSON(201, user)
}Response Template
## Go API Code Review Results
**Project**: [name]
**Framework**: Gin 1.9 | **Go**: 1.22
### Router Organization
| Status | File | Issue |
|--------|------|-------|
| MEDIUM | main.go | 40+ routes in single file |
### Middleware
| Status | File | Issue |
|--------|------|-------|
| HIGH | handlers/user.go | Auth check in handler, not middleware |
### Validation
| Status | File | Issue |
|--------|------|-------|
| HIGH | handlers/user.go:34 | No request validation |
### Error Handling
| Status | File | Issue |
|--------|------|-------|
| HIGH | handlers/product.go | Inconsistent error response format |
### Recommended Actions
1. [ ] Split routes into separate files by resource
2. [ ] Extract auth logic to middleware
3. [ ] Add request struct validation
4. [ ] Implement standard error response formatBest Practices
- Router: Group by resource, version API
- Middleware: Proper order, reusable
- Validation: Use validator tags
- Errors: Standard format, hide internals
- Docs: Generate OpenAPI from code
Integration
go-reviewer: General Go patternssecurity-scanner: API securityapi-documenter: OpenAPI documentation