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
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Reviews Go API projects using Gin, Echo, Fiber, or Chi for routing, middleware, and API patterns.
github.com/gin-gonic/gin importgithub.com/labstack/echo importgithub.com/gofiber/fiber importgithub.com/go-chi/chi importhandlers/, routes/, middleware/ directories**Framework**: Gin v1.9+
**Router**: Group-based routing
**Middleware**: Auth, CORS, Logger, Recovery
**Validation**: go-playground/validator
**Docs**: Swagger/OpenAPI
AskUserQuestion:
"Which areas to review?"
Options:
- Full API review (recommended)
- Router and handler patterns
- Middleware implementation
- Request validation
- Error handling and responses
multiSelect: true
| 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)
}
| 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()
}
}
| 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
}
| 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",
})
}
// 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)
}
## 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 format
go-reviewer: General Go patternssecurity-scanner: API securityapi-documenter: OpenAPI documentation