A lightweight Go package that wraps typed handler functions into http.Handler values. It handles request decoding, validation, JSON encoding, error writing, and panic recovery — so your handler functions can focus purely on business logic.
go get github.com/canpacis/handlerDefine your request and response types, then pass a typed function to handler.Of:
package main
import (
"context"
"net/http"
handler "github.com/canpacis/handler"
)
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func createUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
// Your business logic here
return &CreateUserResponse{ID: 1, Name: req.Name, Email: req.Email}, nil
}
func main() {
mux := http.NewServeMux()
mux.Handle("POST /users", handler.Of(createUser))
http.ListenAndServe(":8080", mux)
}The handler automatically decodes the JSON request body into CreateUserRequest, calls createUser, and JSON-encodes the response. If your function returns nil as the response, the handler writes a 204 No Content status instead.
The package provides several convenience wrappers depending on whether your handler needs a request payload and/or produces a response body.
The core wrapper. Decodes the request, calls your function, encodes the response.
h := handler.Of(func(ctx context.Context, req *MyRequest) (*MyResponse, error) {
return &MyResponse{Message: "ok"}, nil
})For handlers that don't read a request body (e.g. GET endpoints).
h := handler.OfNoPayload(func(ctx context.Context) (*MyResponse, error) {
items, err := db.ListAll(ctx)
return &MyResponse{Items: items}, err
})For handlers that perform an action and return 204 No Content on success.
h := handler.OfNoResponse(func(ctx context.Context, req *DeleteRequest) error {
return db.Delete(ctx, req.ID)
})For simple trigger-style endpoints.
h := handler.OfAction(func(ctx context.Context) error {
return cache.Flush(ctx)
})For server-sent events, chunked transfers, or any format where you write the response yourself via an io.Writer.
h := handler.OfStream(func(ctx context.Context, req *SearchRequest, w io.Writer) error {
for _, result := range search(req.Query) {
fmt.Fprintf(w, "data: %s\n\n", result)
}
return nil
})h := handler.OfActionStream(func(ctx context.Context, w io.Writer) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for t := range ticker.C {
fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
}
return nil
})Register a package-level validator once at startup. The validator receives the decoded payload and should return an error if it is invalid. This integrates naturally with struct validation libraries like go-playground/validator.
import (
"github.com/go-playground/validator/v10"
handler "github.com/canpacis/handler"
)
func main() {
validate := validator.New()
handler.SetDefaultValidator(func(v any) error {
return validate.Struct(v)
})
// ...
}Your payload struct can then use standard validate tags:
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}If validation fails, the error writer is called with an error of kind ValidatePayloadErrorKind.
By default, all errors result in a 500 Internal Server Error with the error message as plain text. Replace the error writer to customise this behaviour — for example, to return structured JSON errors or map specific error kinds to appropriate HTTP status codes.
handler.SetDefaultErrorWriter(func(w http.ResponseWriter, r *http.Request, err error) error {
var handlerErr *handler.Error
if errors.As(err, &handlerErr) {
switch handlerErr.Kind {
case handler.DecodePayloadErrorKind, handler.ValidatePayloadErrorKind:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
}
w.WriteHeader(http.StatusInternalServerError)
_, e := w.Write([]byte(`{"error":"internal server error"}`))
return e
})The *handler.Error type exposes a Kind field and wraps the underlying error, so you can use errors.Is and errors.As on it.
By default, panics are re-panicked, preserving standard Go behaviour. You can install a custom panic handler to log the stack trace and return a graceful error response instead of crashing:
handler.SetDefaultPanicHandler(func(w http.ResponseWriter, r *http.Request, p any) {
log.Printf("panic: %v\n%s", p, debug.Stack())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"unexpected error"}`))
})When a handler function only receives a context.Context (e.g. via OfNoPayload), you can still access the underlying http.ResponseWriter to set headers or perform other low-level operations:
handler.OfNoPayload(func(ctx context.Context) (*MyResponse, error) {
w := handler.ResponseWriter(ctx)
w.Header().Set("X-Request-ID", generateID())
return &MyResponse{}, nil
})ResponseWriter returns nil if called outside of a handler context.
| Constant | Description |
|---|---|
UnknownErrorKind |
Unclassified error origin |
DecodePayloadErrorKind |
Failed to decode the request payload |
ValidatePayloadErrorKind |
Payload failed validation |
CallMethodErrorKind |
The handler function returned an error |
EncodeResponseErrorKind |
Failed to JSON-encode the response |