Go Create 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" "strconv"
"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/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 *usecase.ResourceCreateUseCase resourceListUseCase *usecase.ResourceListUseCase resourceUpdateUseCase *usecase.ResourceUpdateUseCase resourceDeleteUseCase *usecase.ResourceDeleteUseCase errorHandler response.ErrorHandler logger logger.Logger }
func NewResourceHandler( resourceCreateUseCase *usecase.ResourceCreateUseCase, resourceListUseCase *usecase.ResourceListUseCase, resourceUpdateUseCase *usecase.ResourceUpdateUseCase, resourceDeleteUseCase *usecase.ResourceDeleteUseCase, errorHandler response.ErrorHandler, logger logger.Logger, ) *ResourceHandler { return &ResourceHandler{ resourceCreateUseCase: resourceCreateUseCase, resourceListUseCase: resourceListUseCase, resourceUpdateUseCase: resourceUpdateUseCase, resourceDeleteUseCase: resourceDeleteUseCase, errorHandler: errorHandler, logger: logger, } }
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
List (GET /resource)
// @Summary List resources // @Description Retrieves all resources // @Tags Resources // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Envelope[[]dto.ResourceResponse] "Successfully retrieved resources" // @Failure 401 {object} errs.Error "Invalid credentials" // @Failure 500 {object} errs.Error "Internal server error" // @Router /api/v1/resources [get] func (h *ResourceHandler) ListResources(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
output, err := h.resourceListUseCase.Execute(ctx)
if err != nil {
h.logger.Error("failed to list resources", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
resources := make([]dto.ResourceResponse, len(output.Resources))
for i, resource := range output.Resources {
resources[i] = dto.ResourceResponse{
ID: resource.ID,
Name: resource.Name,
// ... map other fields
}
}
err = response.JSON(w, http.StatusOK, resources, http.Header{})
if err != nil {
h.logger.Error("failed to write response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
Create (POST /resource)
// @Summary Create resource // @Description Creates a new resource // @Tags Resources // @Accept json // @Produce json // @Security BearerAuth // @Param request body dto.CreateResourceRequest true "Resource data" // @Success 201 {object} response.Envelope[dto.CreateResourceResponse] "Successfully created resource" // @Failure 422 {object} errs.Error "Invalid request format or validation error" // @Failure 401 {object} errs.Error "Invalid credentials" // @Failure 500 {object} errs.Error "Internal server error" // @Router /api/v1/resources [post] func (h *ResourceHandler) CreateResource(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var createRequest dto.CreateResourceRequest
err := request.ReadJSON(w, r, &createRequest)
if err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
input := usecase.ResourceCreateInput{
Name: createRequest.Name,
// ... map other fields
}
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
}
createResponse := dto.CreateResourceResponse{
ID: output.ID,
Name: output.Name,
// ... map other fields
}
err = response.JSON(w, http.StatusCreated, createResponse, http.Header{})
if err != nil {
h.logger.Error("failed to write response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
Update (PUT /resource/:id)
// @Summary Update resource // @Description Updates an existing resource // @Tags Resources // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Resource ID" // @Param request body dto.UpdateResourceRequest true "Resource data" // @Success 204 "Successfully updated resource" // @Failure 422 {object} errs.Error "Invalid request format or validation error" // @Failure 401 {object} errs.Error "Invalid credentials" // @Failure 404 {object} errs.Error "Resource not found" // @Failure 500 {object} errs.Error "Internal server error" // @Router /api/v1/resources/{id} [put] func (h *ResourceHandler) UpdateResource(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var updateRequest dto.UpdateResourceRequest
err := request.ReadJSON(w, r, &updateRequest)
if err != nil {
h.logger.Error("failed to parse request body", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("invalid resource ID", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
input := usecase.ResourceUpdateInput{
ID: id,
Name: updateRequest.Name,
// ... map other fields
}
err = h.resourceUpdateUseCase.Execute(ctx, input)
if err != nil {
h.logger.Error("failed to update resource", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
response.NoContent(w)
}
Delete (DELETE /resource/:id)
// @Summary Delete resource // @Description Deletes an existing resource // @Tags Resources // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Resource ID" // @Success 204 "Successfully deleted resource" // @Failure 401 {object} errs.Error "Invalid credentials" // @Failure 404 {object} errs.Error "Resource not found" // @Failure 500 {object} errs.Error "Internal server error" // @Router /api/v1/resources/{id} [delete] func (h *ResourceHandler) DeleteResource(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("invalid resource ID", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
input := usecase.ResourceDeleteInput{
ID: id,
}
err = h.resourceDeleteUseCase.Execute(ctx, input)
if 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 /resource/:id)
// @Summary Get resource // @Description Retrieves a resource by ID // @Tags Resources // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Resource ID" // @Success 200 {object} response.Envelope[dto.ResourceResponse] "Successfully retrieved resource" // @Failure 401 {object} errs.Error "Invalid credentials" // @Failure 404 {object} errs.Error "Resource not found" // @Failure 500 {object} errs.Error "Internal server error" // @Router /api/v1/resources/{id} [get] func (h *ResourceHandler) GetResource(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
h.logger.Error("invalid resource ID", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
input := usecase.ResourceGetInput{
ID: id,
}
output, err := h.resourceGetUseCase.Execute(ctx, input)
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
}
err = response.JSON(w, http.StatusOK, resourceResponse, http.Header{})
if err != nil {
h.logger.Error("failed to write response", logger.Error(err))
h.errorHandler.Error(w, err)
return
}
}
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 BearerAuth if 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"
-
@Success: Status code with response type
-
200: {object} response.Envelope[dto.ResourceResponse]
-
201: {object} response.Envelope[dto.CreateResourceResponse]
-
204: No content, just description string
-
@Failure: Common errors (401, 404, 422, 500) with {object} errs.Error
-
@Router: /api/v1/resources/{id} [method]
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, }
Error Handling Pattern
Every error follows this pattern:
if err != nil { h.logger.Error("descriptive error message", logger.Error(err)) h.errorHandler.Error(w, err) return }
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 response" - JSON response write error
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
-
Struct: Include all use cases, response.ErrorHandler , and logger.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: Use chi.URLParam(r, "paramName") and strconv.ParseUint for IDs
-
Error handling: Always log error and call h.errorHandler.Error(w, err) then return
-
Response mapping: Map use case output to DTO, don't return use case outputs directly
-
Success responses:
-
List/Get: Use response.JSON(w, http.StatusOK, data, http.Header{})
-
Create: Use response.JSON(w, http.StatusCreated, data, http.Header{})
-
Update/Delete: Use response.NoContent(w)
-
Swagger: MUST add complete swagger annotations for every handler method
-
No comments: Do not add redundant comments inside method bodies
-
Validation: Run make lint and make update-swagger after generation
Workflow
-
Create handler struct with use case dependencies
-
Implement constructor NewResourceHandler
-
Implement handler methods following patterns above
-
Add swagger annotations to all methods
-
Add Fx wiring to module's fx.go
-
Run make lint and make nilaway to verify the static tests
-
Run make update-swagger to regenerate swagger docs