Skip to content

A lightweight Go package that wraps typed handler functions into `http.Handler` values.

License

Notifications You must be signed in to change notification settings

canpacis/handler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

handler

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.


Getting Started

Installation

go get github.com/canpacis/handler

Basic Example

Define 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.


Usage

Handler Variants

The package provides several convenience wrappers depending on whether your handler needs a request payload and/or produces a response body.

Of — payload in, response out

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
})

OfNoPayload — no request body, response out

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
})

OfNoResponse — payload in, no response body

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)
})

OfAction — no payload, no response body

For simple trigger-style endpoints.

h := handler.OfAction(func(ctx context.Context) error {
    return cache.Flush(ctx)
})

OfStream — payload in, streaming response

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
})

OfActionStream — no payload, streaming response

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
})

Validation

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.


Custom Error Handling

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.


Panic Recovery

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"}`))
})

Accessing the Response Writer from Context

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.


Error Kinds Reference

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

About

A lightweight Go package that wraps typed handler functions into `http.Handler` values.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages