Initial commit
This commit is contained in:
356
skills/go-api-reviewer/SKILL.md
Normal file
356
skills/go-api-reviewer/SKILL.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
name: go-api-reviewer
|
||||
description: |
|
||||
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
|
||||
---
|
||||
|
||||
# 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/gin` import
|
||||
- `github.com/labstack/echo` import
|
||||
- `github.com/gofiber/fiber` import
|
||||
- `github.com/go-chi/chi` import
|
||||
- `handlers/`, `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/OpenAPI
|
||||
```
|
||||
|
||||
### Step 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: true
|
||||
```
|
||||
|
||||
## Detection 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 |
|
||||
|
||||
```go
|
||||
// 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 |
|
||||
|
||||
```go
|
||||
// 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 |
|
||||
|
||||
```go
|
||||
// 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 |
|
||||
|
||||
```go
|
||||
// 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)
|
||||
```go
|
||||
// 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 format
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
1. **Router**: Group by resource, version API
|
||||
2. **Middleware**: Proper order, reusable
|
||||
3. **Validation**: Use validator tags
|
||||
4. **Errors**: Standard format, hide internals
|
||||
5. **Docs**: Generate OpenAPI from code
|
||||
|
||||
## Integration
|
||||
- `go-reviewer`: General Go patterns
|
||||
- `security-scanner`: API security
|
||||
- `api-documenter`: OpenAPI documentation
|
||||
Reference in New Issue
Block a user