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
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:
go
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)
go
// @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)
go
// @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)
go
// @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)
go
// @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)
go
// @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
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 (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
go
// 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
go
// 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:
go
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:
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, 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: Use
chi.URLParam(r, "paramName")andstrconv.ParseUintfor 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)
- •List/Get: Use
- •Swagger: MUST add complete swagger annotations for every handler method
- •No comments: Do not add redundant comments inside method bodies
- •Validation: Run
make lintandmake update-swaggerafter 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 lintandmake nilawayto verify the static tests - •Run
make update-swaggerto regenerate swagger docs