go-chi-handler
npx skills add https://github.com/cristiano-pacheco/ai-rules --skill go-chi-handler
Agent 安装分布
Skill 文档
Go Chi Handler
Generate Chi HTTP handler implementations for a Go backend.
Handler Structure
Location: internal/modules/<module>/http/chi/handler/<resource>_handler.go
package handler
import (
"net/http"
"fmt"
"strconv"
"strings"
brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs"
"github.com/cristiano-pacheco/bricks/pkg/http/request"
"github.com/cristiano-pacheco/bricks/pkg/http/response"
"github.com/cristiano-pacheco/bricks/pkg/logger"
"github.com/cristiano-pacheco/bricks/pkg/ucdecorator"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/http/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/usecase"
"github.com/go-chi/chi/v5"
)
type ResourceHandler struct {
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
resourceListUseCase ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput]
resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput]
resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput]
resourceGetUseCase ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput]
errorHandler response.ErrorHandler
logger logger.Logger
}
func NewResourceHandler(
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput],
resourceListUseCase ucdecorator.UseCase[usecase.ResourceListInput, usecase.ResourceListOutput],
resourceUpdateUseCase ucdecorator.UseCase[usecase.ResourceUpdateInput, usecase.ResourceUpdateOutput],
resourceDeleteUseCase ucdecorator.UseCase[usecase.ResourceDeleteInput, usecase.ResourceDeleteOutput],
resourceGetUseCase ucdecorator.UseCase[usecase.ResourceGetInput, usecase.ResourceGetOutput],
errorHandler response.ErrorHandler,
logger logger.Logger,
) *ResourceHandler {
return &ResourceHandler{
resourceCreateUseCase: resourceCreateUseCase,
resourceListUseCase: resourceListUseCase,
resourceUpdateUseCase: resourceUpdateUseCase,
resourceDeleteUseCase: resourceDeleteUseCase,
resourceGetUseCase: resourceGetUseCase,
errorHandler: errorHandler,
logger: logger,
}
}
Key points:
- Use cases are always
ucdecorator.UseCase[Input, Output]generic interface â never concrete*usecase.ResourceUseCasepointers - Import
brickserrswith alias:brickserrs "github.com/cristiano-pacheco/bricks/pkg/errs" - Constructor returns pointer
*ResourceHandler
DTOs (Data Transfer Objects)
Request and response DTOs are defined in internal/modules/<module>/http/dto/<resource>_dto.go.
Typical DTO structure:
package dto
type CreateResourceRequest struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type CreateResourceResponse struct {
ID uint64 `json:"id"`
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type UpdateResourceRequest struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
type ResourceResponse struct {
ID uint64 `json:"id"`
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
Key points:
- DTOs live in the HTTP transport layer, separate from use case inputs or models
- Use JSON tags for serialization
- Keep DTOs focused on HTTP contract, not domain logic
Handler Method Patterns
All handler methods use the Handle prefix: HandleListResources, HandleCreateResource, etc.
List (GET /resources)
// @Summary List resources
// @Description Retrieves all resources
// @Tags Resources
// @Accept json
// @Produce json
// @Success 200 {object} response.Envelope[[]dto.ResourceResponse] "Successfully retrieved resources"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources [get]
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
output, err := h.resourceListUseCase.Execute(ctx, usecase.ResourceListInput{})
if err != nil {
h.logger.Error("failed to list resources", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
resources := make([]dto.ResourceResponse, 0, len(output.Resources))
for _, resource := range output.Resources {
resources = append(resources, dto.ResourceResponse{
ID: resource.ID,
Name: resource.Name,
// ... map other fields
})
}
if err = response.JSON(w, http.StatusOK, resources, http.Header{}); err != nil {
h.logger.Error("failed to write list resources response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
Create (POST /resources)
// @Summary Create resource
// @Description Creates a new resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param request body dto.CreateResourceRequest true "Resource data"
// @Success 201 {object} response.Envelope[dto.CreateResourceResponse] "Successfully created resource"
// @Failure 422 {object} brickserrs.Error "Invalid request format or validation error"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources [post]
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var createRequest dto.CreateResourceRequest
if err := request.ReadJSON(w, r, &createRequest); err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
output, err := h.resourceCreateUseCase.Execute(ctx, usecase.ResourceCreateInput{
Name: createRequest.Name,
// ... map other fields
})
if err != nil {
h.logger.Error("failed to create resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
createResponse := dto.CreateResourceResponse{
ID: output.ID,
Name: output.Name,
// ... map other fields
}
if err = response.JSON(w, http.StatusCreated, createResponse, http.Header{}); err != nil {
h.logger.Error("failed to write create resource response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
Update (PUT /resources/:id)
// @Summary Update resource
// @Description Updates an existing resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Param request body dto.UpdateResourceRequest true "Resource data"
// @Success 204 "Successfully updated resource"
// @Failure 422 {object} brickserrs.Error "Invalid request format or validation error"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [put]
func (h *ResourceHandler) HandleUpdateResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
var updateRequest dto.UpdateResourceRequest
if err = request.ReadJSON(w, r, &updateRequest); err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
if _, err = h.resourceUpdateUseCase.Execute(ctx, usecase.ResourceUpdateInput{
ID: id,
Name: updateRequest.Name,
// ... map other fields
}); err != nil {
h.logger.Error("failed to update resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
response.NoContent(w)
}
Delete (DELETE /resources/:id)
// @Summary Delete resource
// @Description Deletes an existing resource
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Success 204 "Successfully deleted resource"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [delete]
func (h *ResourceHandler) HandleDeleteResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
if _, err = h.resourceDeleteUseCase.Execute(ctx, usecase.ResourceDeleteInput{ID: id}); err != nil {
h.logger.Error("failed to delete resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
response.NoContent(w)
}
Get by ID (GET /resources/:id)
// @Summary Get resource
// @Description Retrieves a resource by ID
// @Tags Resources
// @Accept json
// @Produce json
// @Param id path int true "Resource ID"
// @Success 200 {object} response.Envelope[dto.ResourceResponse] "Successfully retrieved resource"
// @Failure 404 {object} brickserrs.Error "Resource not found"
// @Failure 500 {object} brickserrs.Error "Internal server error"
// @Router /api/v1/resources/{id} [get]
func (h *ResourceHandler) HandleGetResource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := h.parseUintPathParam(r, "id")
if err != nil {
h.logger.Error("invalid resource id", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
output, err := h.resourceGetUseCase.Execute(ctx, usecase.ResourceGetInput{ID: id})
if err != nil {
h.logger.Error("failed to get resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
resourceResponse := dto.ResourceResponse{
ID: output.ID,
Name: output.Name,
// ... map other fields
}
if err = response.JSON(w, http.StatusOK, resourceResponse, http.Header{}); err != nil {
h.logger.Error("failed to write get resource response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
URL Param Helper (private method)
Handlers that parse path parameters should use a private method on the struct â not a standalone function:
func (h *ResourceHandler) parseUintPathParam(r *http.Request, paramName string) (uint64, error) {
value := strings.TrimSpace(chi.URLParam(r, paramName))
if value == "" {
return 0, h.newBadRequestError(fmt.Sprintf("missing path param %q", paramName))
}
parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return 0, h.newBadRequestError(fmt.Sprintf("invalid path param %q", paramName))
}
return parsed, nil
}
func (h *ResourceHandler) newBadRequestError(message string) *brickserrs.Error {
return brickserrs.New("MODULE_90", message, http.StatusBadRequest, nil)
}
Request/Response Mapping
Handler methods bridge HTTP requests/responses (DTOs from internal/modules/<module>/http/dto) with use case inputs/outputs.
Request to Use Case Input
// Decode request
var req dto.CreateResourceRequest
err := request.ReadJSON(w, r, &req)
if err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
// Map to use case input
input := usecase.ResourceCreateInput{
Field1: req.Field1,
Field2: req.Field2,
}
Use Case Output to Response
// Execute use case
output, err := h.resourceCreateUseCase.Execute(ctx, input)
if err != nil {
h.logger.Error("failed to create resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
// Map to response DTO
response := dto.CreateResourceResponse{
ID: output.ID,
Field1: output.Field1,
Field2: output.Field2,
}
Swagger Annotation Rules
- @Summary: Brief action description (e.g., “List resources”, “Create resource”)
- @Description: Full description of what the endpoint does
- @Tags: Plural resource name (e.g., “Resources”, “Contacts”)
- @Accept: Always
json - @Produce: Always
json - @Security: Add
BearerAuthif authentication required - @Param: Define path params and request body
- Path param:
@Param id path int true "Resource ID" - Request body:
@Param request body dto.CreateResourceRequest true "Resource data"
- Path param:
- @Success: Status code with response type
- 200:
{object} response.Envelope[dto.ResourceResponse] - 201:
{object} response.Envelope[dto.CreateResourceResponse] - 204: No content, just description string
- 200:
- @Failure: Common errors (404, 422, 500) with
{object} brickserrs.Error(note the alias) - @Router:
/api/v1/resources/{id} [method]
Error Handling Pattern
Every error in every handler method must follow this exact pattern â no exceptions, including input validation and URL param parse errors:
if err != nil {
h.logger.Error("descriptive error message", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
Standard error messages:
"failed to parse request body"â JSON decode error"invalid resource id"â URL param parsing error"failed to list resources"â List use case error"failed to create resource"â Create use case error"failed to update resource"â Update use case error"failed to delete resource"â Delete use case error"failed to get resource"â Get use case error"failed to write list resources response"â JSON response write error (include the operation name)
Fx Wiring
Add to internal/modules/<module>/fx.go:
fx.Provide(handler.NewResourceHandler),
The handler is typically provided to the router, not exposed as a port.
Critical Rules
- No standalone functions: When a file contains a struct with methods, never add package-level standalone functions. All helpers must be private methods on the struct (e.g.,
parseUintPathParam,newBadRequestError). - Struct: Include all use cases,
response.ErrorHandler, andlogger.Logger - Constructor: Must return pointer
*ResourceHandler - Context: Always get from request:
ctx := r.Context() - Request decoding: Use
request.ReadJSON(w, r, &dto)â declare variable first, then call ReadJSON - URL params: Parse via a private
parseUintPathParammethod usingchi.URLParam+strconv.ParseUint - Error handling: Always call
h.logger.Error(...)ANDh.errorHandler.Error(w, err)then return â for ALL errors, including validation/input parse errors - Response mapping: Map use case output to DTO; never return use case outputs directly
- Success responses:
- List/Get:
response.JSON(w, http.StatusOK, data, http.Header{}) - Create:
response.JSON(w, http.StatusCreated, data, http.Header{}) - Update/Delete:
response.NoContent(w)
- List/Get:
- Swagger: Must add complete swagger annotations for every handler method; use
brickserrs.Error(noterrs.Error) in@Failurelines - No comments: Do not add redundant comments inside method bodies
- Validation: Run
make lintandmake update-swaggerafter generation
Anti-Patterns (NEVER DO)
These patterns have appeared in the codebase and must not be repeated:
â Raw w.WriteHeader instead of response.NoContent
// BAD
w.WriteHeader(http.StatusNoContent)
// GOOD
response.NoContent(w)
â Standalone package-level functions in handler files
// BAD â standalone functions at package level violate the no-standalone-functions rule
func isResourceEmpty(output usecase.ResourceGetOutput) bool {
return output.Name == ""
}
func firstNonEmpty(values ...string) string { ... }
// GOOD â private methods on the handler struct
func (h *ResourceHandler) isResourceEmpty(output usecase.ResourceGetOutput) bool {
return output.Name == ""
}
func (h *ResourceHandler) firstNonEmpty(values ...string) string { ... }
â Missing logger call before errorHandler for validation/parse errors
// BAD â skips the logger
if parseErr != nil {
h.errorHandler.Error(w, brickserrs.New("MODULE_90", "invalid id", http.StatusBadRequest, nil))
return
}
// GOOD â always log before handling
if parseErr != nil {
h.logger.Error("invalid resource id", logger.Error(parseErr))
h.errorHandler.Error(w, parseErr)
return
}
â Inline brickserrs.New() with hardcoded error codes in handler bodies
Errors must be defined in the module’s errs/ package (use the go-error skill), not created ad-hoc inside handler method bodies.
// BAD â hardcoded error codes inline in handler logic
h.errorHandler.Error(w, brickserrs.New("CATALOG_90", "invalid category_id", http.StatusBadRequest, nil))
// GOOD â reference a named error from the module's errs/ package
h.errorHandler.Error(w, errs.ErrInvalidCategoryID)
The exception is the newBadRequestError private method used for URL param parsing â that is the one acceptable place for inline construction, scoped to a helper method on the struct.
â Concrete use case types in struct fields and constructor
// BAD â concrete pointer types
type ResourceHandler struct {
resourceCreateUseCase *usecase.ResourceCreateUseCase
}
// GOOD â generic ucdecorator interface
type ResourceHandler struct {
resourceCreateUseCase ucdecorator.UseCase[usecase.ResourceCreateInput, usecase.ResourceCreateOutput]
}
â Handler method names without Handle prefix
// BAD
func (h *ResourceHandler) ListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) CreateResource(w http.ResponseWriter, r *http.Request) { ... }
// GOOD
func (h *ResourceHandler) HandleListResources(w http.ResponseWriter, r *http.Request) { ... }
func (h *ResourceHandler) HandleCreateResource(w http.ResponseWriter, r *http.Request) { ... }
â Wrong error type in swagger @Failure annotations
// BAD â errs.Error is not the correct reference
// @Failure 500 {object} errs.Error "Internal server error"
// GOOD â use the brickserrs alias
// @Failure 500 {object} brickserrs.Error "Internal server error"
Workflow
- Create handler struct with use case dependencies (using
ucdecorator.UseCase[Input, Output]) - Implement constructor
NewResourceHandler - Implement handler methods with
Handleprefix following patterns above - Add private
parseUintPathParam+newBadRequestErrormethods if path params are needed - Add swagger annotations to all methods (using
brickserrs.Errorin@Failure) - Add Fx wiring to module’s
fx.go - Run
make lintandmake nilawayto verify static tests - Run
make update-swaggerto regenerate swagger docs