Claude-skill-registry handler-generator
Generates new Go HTTP handlers following Ishkul patterns. Creates handler with proper request/response types, error handling, validation, structured logging, and matching test file. Use when adding new API endpoints.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/handler-generator" ~/.claude/skills/majiayu000-claude-skill-registry-handler-generator && rm -rf "$T"
manifest:
skills/data/handler-generator/SKILL.mdsource content
Go Handler Generator
Creates new Go HTTP handlers following Ishkul's established patterns.
What Gets Created
When generating a new handler, create:
- Handler file:
backend/internal/handlers/resource_name.go - Test file:
backend/internal/handlers/resource_name_test.go - Route registration: Update
backend/cmd/server/main.go - Models (if needed):
backend/internal/models/resource_name.go
Handler Template
package handlers import ( "encoding/json" "net/http" "log/slog" "github.com/mesbahtanvir/ishkul/backend/internal/middleware" "github.com/mesbahtanvir/ishkul/backend/internal/models" "github.com/mesbahtanvir/ishkul/backend/pkg/firebase" ) // ============================================================================= // Request/Response Types // ============================================================================= // CreateResourceRequest represents the request body for creating a resource type CreateResourceRequest struct { Title string `json:"title"` Description string `json:"description,omitempty"` } // ResourceResponse represents a resource in API responses type ResourceResponse struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description,omitempty"` UserID string `json:"userId"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } // ListResourcesResponse represents the response for listing resources type ListResourcesResponse struct { Resources []ResourceResponse `json:"resources"` Total int `json:"total"` } // ============================================================================= // Error Codes (use existing from auth.go or add new ones) // ============================================================================= const ( ErrCodeResourceNotFound = "RESOURCE_NOT_FOUND" ErrCodeResourceExists = "RESOURCE_EXISTS" ) // ============================================================================= // Main Handler (Router) // ============================================================================= // ResourcesHandler routes requests to the appropriate handler based on path and method // Handles: // - POST /api/resources - Create resource // - GET /api/resources - List user's resources // - GET /api/resources/{id} - Get single resource // - PUT /api/resources/{id} - Update resource // - DELETE /api/resources/{id} - Delete resource func ResourcesHandler(w http.ResponseWriter, r *http.Request) { // Parse resource ID from path if present // Path: /api/resources or /api/resources/{id} resourceID := extractResourceID(r.URL.Path, "/api/resources/") if resourceID == "" { // Root path: /api/resources switch r.Method { case http.MethodPost: createResource(w, r) case http.MethodGet: listResources(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } else { // Resource path: /api/resources/{id} switch r.Method { case http.MethodGet: getResource(w, r, resourceID) case http.MethodPut: updateResource(w, r, resourceID) case http.MethodDelete: deleteResource(w, r, resourceID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } } // ============================================================================= // Individual Handlers // ============================================================================= // createResource handles POST /api/resources func createResource(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // 1. Get authenticated user from context (set by auth middleware) userID := middleware.GetUserID(ctx) if userID == "" { sendErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, "User not authenticated") return } // 2. Parse request body var req CreateResourceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendErrorResponse(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid request format") return } // 3. Validate input if req.Title == "" { sendErrorResponse(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Title is required") return } if len(req.Title) > 200 { sendErrorResponse(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Title must be 200 characters or less") return } // 4. Create resource in Firestore firestoreClient, err := firebase.GetFirestoreClient(ctx) if err != nil { appLogger.Error("firestore_client_error", slog.String("error", err.Error()), slog.String("user_id", userID), ) sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Database connection failed") return } resource := models.Resource{ ID: generateID(), // Use UUID or similar Title: req.Title, Description: req.Description, UserID: userID, CreatedAt: time.Now(), UpdatedAt: time.Now(), } _, err = firestoreClient.Collection("resources").Doc(resource.ID).Set(ctx, resource) if err != nil { appLogger.Error("resource_create_error", slog.String("error", err.Error()), slog.String("user_id", userID), ) sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to create resource") return } // 5. Log success appLogger.Info("resource_created", slog.String("resource_id", resource.ID), slog.String("user_id", userID), slog.String("title", resource.Title), ) // 6. Return response response := ResourceResponse{ ID: resource.ID, Title: resource.Title, Description: resource.Description, UserID: resource.UserID, CreatedAt: resource.CreatedAt.Format(time.RFC3339), UpdatedAt: resource.UpdatedAt.Format(time.RFC3339), } JSONCreated(w, response) } // listResources handles GET /api/resources func listResources(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID := middleware.GetUserID(ctx) if userID == "" { sendErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, "User not authenticated") return } firestoreClient, err := firebase.GetFirestoreClient(ctx) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Database connection failed") return } // Query user's resources docs, err := firestoreClient.Collection("resources"). Where("userId", "==", userID). OrderBy("createdAt", firestore.Desc). Limit(100). // Always limit queries Documents(ctx). GetAll() if err != nil { appLogger.Error("resources_list_error", slog.String("error", err.Error()), slog.String("user_id", userID), ) sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to fetch resources") return } resources := make([]ResourceResponse, 0, len(docs)) for _, doc := range docs { var resource models.Resource if err := doc.DataTo(&resource); err != nil { continue // Skip invalid documents } resources = append(resources, ResourceResponse{ ID: resource.ID, Title: resource.Title, Description: resource.Description, UserID: resource.UserID, CreatedAt: resource.CreatedAt.Format(time.RFC3339), UpdatedAt: resource.UpdatedAt.Format(time.RFC3339), }) } JSONSuccess(w, ListResourcesResponse{ Resources: resources, Total: len(resources), }) } // getResource handles GET /api/resources/{id} func getResource(w http.ResponseWriter, r *http.Request, resourceID string) { ctx := r.Context() userID := middleware.GetUserID(ctx) if userID == "" { sendErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, "User not authenticated") return } firestoreClient, err := firebase.GetFirestoreClient(ctx) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Database connection failed") return } doc, err := firestoreClient.Collection("resources").Doc(resourceID).Get(ctx) if err != nil { if status.Code(err) == codes.NotFound { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to fetch resource") return } var resource models.Resource if err := doc.DataTo(&resource); err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to parse resource") return } // Check ownership if resource.UserID != userID { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } JSONSuccess(w, ResourceResponse{ ID: resource.ID, Title: resource.Title, Description: resource.Description, UserID: resource.UserID, CreatedAt: resource.CreatedAt.Format(time.RFC3339), UpdatedAt: resource.UpdatedAt.Format(time.RFC3339), }) } // updateResource handles PUT /api/resources/{id} func updateResource(w http.ResponseWriter, r *http.Request, resourceID string) { ctx := r.Context() userID := middleware.GetUserID(ctx) if userID == "" { sendErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, "User not authenticated") return } var req CreateResourceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendErrorResponse(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid request format") return } firestoreClient, err := firebase.GetFirestoreClient(ctx) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Database connection failed") return } // Get existing resource docRef := firestoreClient.Collection("resources").Doc(resourceID) doc, err := docRef.Get(ctx) if err != nil { if status.Code(err) == codes.NotFound { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to fetch resource") return } var resource models.Resource if err := doc.DataTo(&resource); err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to parse resource") return } // Check ownership if resource.UserID != userID { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } // Update fields updates := []firestore.Update{ {Path: "updatedAt", Value: time.Now()}, } if req.Title != "" { updates = append(updates, firestore.Update{Path: "title", Value: req.Title}) } if req.Description != "" { updates = append(updates, firestore.Update{Path: "description", Value: req.Description}) } _, err = docRef.Update(ctx, updates) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to update resource") return } appLogger.Info("resource_updated", slog.String("resource_id", resourceID), slog.String("user_id", userID), ) JSONSuccess(w, map[string]string{"message": "Resource updated"}) } // deleteResource handles DELETE /api/resources/{id} func deleteResource(w http.ResponseWriter, r *http.Request, resourceID string) { ctx := r.Context() userID := middleware.GetUserID(ctx) if userID == "" { sendErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, "User not authenticated") return } firestoreClient, err := firebase.GetFirestoreClient(ctx) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Database connection failed") return } docRef := firestoreClient.Collection("resources").Doc(resourceID) doc, err := docRef.Get(ctx) if err != nil { if status.Code(err) == codes.NotFound { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to fetch resource") return } var resource models.Resource if err := doc.DataTo(&resource); err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to parse resource") return } // Check ownership if resource.UserID != userID { sendErrorResponse(w, http.StatusNotFound, ErrCodeResourceNotFound, "Resource not found") return } _, err = docRef.Delete(ctx) if err != nil { sendErrorResponse(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to delete resource") return } appLogger.Info("resource_deleted", slog.String("resource_id", resourceID), slog.String("user_id", userID), ) w.WriteHeader(http.StatusNoContent) } // ============================================================================= // Helpers // ============================================================================= func extractResourceID(path, prefix string) string { if !strings.HasPrefix(path, prefix) { return "" } id := strings.TrimPrefix(path, prefix) // Remove trailing slashes and any sub-paths if idx := strings.Index(id, "/"); idx != -1 { id = id[:idx] } return strings.TrimSpace(id) }
Model Template
If needed, create
backend/internal/models/resource.go:
package models import "time" // Resource represents a user-created resource type Resource struct { ID string `json:"id" firestore:"id"` Title string `json:"title" firestore:"title"` Description string `json:"description,omitempty" firestore:"description,omitempty"` UserID string `json:"userId" firestore:"userId"` CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` }
Route Registration
Add to
backend/cmd/server/main.go:
// In main() after other route registrations: mux.Handle("/api/resources", middleware.Auth(http.HandlerFunc(handlers.ResourcesHandler))) mux.Handle("/api/resources/", middleware.Auth(http.HandlerFunc(handlers.ResourcesHandler)))
Test File Template
See test-generator skill for full Go test template. Key sections:
- Method validation tests
- Authentication tests
- Input validation tests
- Success cases
- Edge cases
- Authorization (ownership) tests
Checklist Before Completing
- Handler file created with proper structure
- Request/response types defined
- Error codes defined/reused
- Input validation implemented
- Ownership checks for user resources
- Structured logging added
- Test file created
- Route registered in main.go
- Run verification:
cd backend && gofmt -w . && go vet ./... && go test ./...
When to Use
- When adding new API endpoints
- When creating CRUD operations for a resource
- When building authenticated endpoints
- When integrating with Firestore