Write Elegant Go Handlers by Packing Boilerplate with a Generic Wrap
The article shows how typical Go HTTP/gRPC handlers repeat five steps of decoding, validation, type conversion, business call, and encoding, and demonstrates a generic Wrap adapter that extracts the repetitive pipeline, leaving handlers thin, type‑safe, and focused solely on business logic.
Identify the pain point
Typical handlers repeat five steps—decode, validate, convert types, call the service, and encode the response—resulting in a lot of boilerplate that looks like a photocopier. Each new endpoint repeats the same "pipeline code".
Solution: give the pipeline a uniform wrapper
Since every endpoint shares the same pipeline, we can write a generic Wrap function that bundles the repetitive work, allowing the handler to do only one thing: invoke business logic.
Step 1: Keep business functions pure
// Domain layer: pure business logic, no HTTP/gRPC dependencies
func (s *Service) Greet(ctx context.Context, in GreetIn) (GreetOut, error) {
user, _ := s.users.Get(in.UserID) // fetch user
msg := "hey " + user.Name + "!" // build greeting
return GreetOut{Message: msg}, nil
}Step 2: Write a generic "universal wrapper"
// Simplified Wrap: packs pipeline logic into a function
func Wrap[In, Out any](
decode func(*http.Request) (In, error), // ① how to decode
business func(context.Context, In) (Out, error), // ② core business
encode func(http.ResponseWriter, Out) error, // ③ how to encode
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
in, _ := decode(r) // automatic decode
out, _ := business(r.Context(), in) // call business
_ = encode(w, out) // automatic encode
})
}Step 3: Register routes in one line
// Before: ~30 lines of boilerplate per endpoint
// After: just three lines
mux.Handle("POST /greet", Wrap(
decodeGreet, // tell Wrap how to decode
svc.Greet, // core business (reused)
encodeGreet, // tell Wrap how to encode
))Real benefits after refactoring
✅ Adding a new endpoint goes from writing 50 lines of boilerplate + 20 lines of business to only 10 lines of decode/encode + 20 lines of business.
✅ Changing validation logic requires a single change that instantly applies to all endpoints.
✅ Test layering: business logic can be unit‑tested without transport concerns; pipeline logic is tested once per transport.
The generic Wrap ensures compile‑time type safety: the type returned by decode must match the input of the business function, and the type returned by the business function must match the input of encode, eliminating runtime panics.
Avoiding pitfalls
Validate should be optional; use a type assertion like any(in).(validator) so only structs that implement Validate() run validation.
Centralise error mapping: translate domain errors (e.g., UserNotFound) to HTTP/gRPC status codes in one place instead of scattering guesses across handlers.
Don’t turn Wrap into a Swiss‑army‑knife that also handles routing, rate‑limiting, or circuit‑breaking—keep it single‑responsibility: the "how" (decode/encode) and the "what" (business) are separate.
This design follows the principle "Handler Thin, Service Fat": handlers contain only transport glue, while services hold all business logic.
Source: Huawei Cloud Community, https://bbs.huaweicloud.com/blogs/477727
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Golang Shines
We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
