Stage 7 · Master
Phase 2 — Organization Service
Handler Layer
Translate HTTP requests into service calls and back — and see, firsthand, the repetition that motivates the next lesson's centralized error handling.
Handlers Translate HTTP to Calls and Back, Nothing Else
internal/handler/organization_handler.go binds a request body or query parameters into a DTO, calls exactly one method on OrganizationService, and maps the result back to a response DTO. It contains no business rule — no status-transition logic, no slug-conflict handling — because every one of those decisions was made in the Service Layer lesson. A handler that starts making decisions of its own is a handler that has quietly grown a second, undocumented service layer.
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/hoa-platform/backend/pkg/apperr"
"github.com/hoa-platform/backend/services/organization/internal/dto"
"github.com/hoa-platform/backend/services/organization/internal/model"
"github.com/hoa-platform/backend/services/organization/internal/repository"
"github.com/hoa-platform/backend/services/organization/internal/service"
)
type OrganizationHandler struct {
svc *service.OrganizationService
}
func NewOrganizationHandler(svc *service.OrganizationService) *OrganizationHandler {
return &OrganizationHandler{svc: svc}
}
// respondError is duplicated, nearly identically, in every handler method
// below. That repetition is deliberate and temporary — the Error Handling
// lesson collapses all six copies into one shared function.
func respondError(c *gin.Context, err error) {
appErr, ok := apperr.As(err)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL", "message": "unexpected error"}})
return
}
status := http.StatusInternalServerError
switch appErr.Code {
case apperr.CodeNotFound:
status = http.StatusNotFound
case apperr.CodeConflict:
status = http.StatusConflict
case apperr.CodeInvalidInput:
status = http.StatusUnprocessableEntity
}
c.JSON(status, gin.H{"error": gin.H{"code": appErr.Code, "message": appErr.Message}})
}
func (h *OrganizationHandler) Create(c *gin.Context) {
var req dto.CreateOrganizationRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, apperr.InvalidInput("invalid request body", nil))
return
}
org, err := h.svc.Create(c.Request.Context(), req)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusCreated, dto.ToResponse(org))
}
func (h *OrganizationHandler) Get(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondError(c, apperr.InvalidInput("invalid organization id", nil))
return
}
org, err := h.svc.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, dto.ToResponse(org))
}
func (h *OrganizationHandler) List(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
filter := repository.ListFilter{
Status: model.Status(c.Query("status")),
Limit: limit,
Offset: offset,
}
orgs, total, err := h.svc.List(c.Request.Context(), filter)
if err != nil {
respondError(c, err)
return
}
responses := make([]dto.OrganizationResponse, len(orgs))
for i := range orgs {
responses[i] = dto.ToResponse(&orgs[i])
}
c.JSON(http.StatusOK, dto.ListOrganizationsResponse{
Data: responses,
Meta: dto.PageMeta{Total: total, Limit: limit, Offset: offset},
})
}
func (h *OrganizationHandler) Update(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondError(c, apperr.InvalidInput("invalid organization id", nil))
return
}
var req dto.UpdateOrganizationRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, apperr.InvalidInput("invalid request body", nil))
return
}
org, err := h.svc.Update(c.Request.Context(), id, req)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, dto.ToResponse(org))
}
func (h *OrganizationHandler) Suspend(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondError(c, apperr.InvalidInput("invalid organization id", nil))
return
}
org, err := h.svc.Suspend(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, dto.ToResponse(org))
}
func (h *OrganizationHandler) Archive(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondError(c, apperr.InvalidInput("invalid organization id", nil))
return
}
org, err := h.svc.Archive(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
c.JSON(http.StatusOK, dto.ToResponse(org))
}
respondError is written once here as a shared function, but every one of the six handler methods above still repeats the same three-line pattern (bind or parse, call the service, respond). That repetition is the point of the next section.
The Repetition This Lesson Deliberately Leaves Unresolved
respondError already centralizes the apperr-to-status mapping. What remains uncentralized is structural: every method still calls respondError explicitly at every possible failure point, and a future seventh handler method will copy the same shape again. The Error Handling lesson later in this phase replaces this pattern with Gin middleware that lets a handler simply return an error and have it mapped automatically — but seeing the repetition first, here, makes that refactor's motivation concrete instead of abstract.
Exercising Every Route by Hand, Before Real Routing Exists
internal/router does not exist until the next lesson, so this verification wires the six handler methods directly onto a throwaway gin.Engine, temporarily, inside a small script — proving the handlers themselves work correctly before REST API Design decides their permanent paths and prefixes.
Applied exercise
Count the repetition yourself instead of taking the lesson's claim on faith
The lesson asserts every handler method repeats the same shape. Verify the claim precisely.
- Count, by hand, how many of the six methods above call uuid.Parse(c.Param("id")) followed by the identical three-line invalid-id error block.
- Count how many call respondError(c, err) immediately after a service call fails.
- Write down, in one sentence, what a seventh method (say, a future Reactivate) would have to duplicate if added today, using this lesson's pattern.
Deliverable
A short note with both counts and the one-sentence prediction for a seventh method.
Completion checks
- The count for uuid.Parse repetition is 4 (Get, Update, Suspend, Archive).
- The prediction correctly anticipates that a seventh method would duplicate the same id-parsing and error-responding boilerplate.
What does the Handler Layer lesson deliberately leave unresolved, on purpose, for the Error Handling lesson to fix?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.