httphandler

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 12, 2024 License: MIT Imports: 4 Imported by: 0

README

go-httphandler

GoDoc Go Report Card License

A zero-dependency HTTP response handler for Go that makes writing HTTP handlers idiomatic and less error-prone.

Features

  • Zero Dependencies: Built entirely on Go's standard library
  • 📄 Built-in Response Types: Support for JSON, plain text, file downloads, and redirects
  • 🛠️ Fluent API: Chain methods to customize responses with headers, cookies, and status codes
  • 🔄 Flexible Request Parsing: Built-in JSON parsing with support for custom decoders
  • 🧩 Easily Extendable: Create custom response types and request decoders
  • 📝 Integrated Logging: Optional logging support for all response types

Why go-httphandler?

Traditional Go HTTP handlers interact directly with http.ResponseWriter, which can lead to several common pitfalls:

// Traditional approach - common pitfalls

// Pitfall 1: Headers must be set before writing the response
router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    user := getUser(r.PathValue("id"))
    if user == nil {
        json.NewEncoder(w).Encode(map[string]string{
            "error": "User not found",
        })
        w.WriteHeader(http.StatusNotFound) // Bug: Too late! Headers can't be set after writing response
        return
    }
    json.NewEncoder(w).Encode(user)
})

// Pitfall 2: Missing returns cause code to continue executing
router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    user := getUser(r.PathValue("id"))
    if user == nil {
        w.WriteHeader(http.StatusNotFound)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "User not found",
        })
        // Missing return! Code continues executing...
    }
    
    // This will still execute!
    json.NewEncoder(w).Encode(user)
})

// go-httphandler approach - prevents both issues by design
router.HandleFunc("GET /users/{id}", httphandler.Handle(func(r *http.Request) httphandler.Responder {
    user := getUser(r.PathValue("id"))
    if user == nil {
        return jsonresp.Error(nil, "User not found", http.StatusNotFound)
    }
    return jsonresp.Success(user)
}))

Installation

go get github.com/alvinchoong/go-httphandler

Usage Examples

Response Types
JSON Response
func getUserHandler(r *http.Request) httphandler.Responder {
    user, err := getUser(r.PathValue("id"))
    if err != nil {
        return jsonresp.InternalServerError(err)
    }
    if user == nil {
        return jsonresp.Error(nil, "User not found", http.StatusNotFound)
    }
    return jsonresp.Success(user)
}

router.HandleFunc("GET /users/{id}", httphandler.Handle(getUserHandler))
File Response
func downloadReportHandler(r *http.Request) httphandler.Responder {
    file := getReport()
    return downloadresp.Attachment(file, "report.pdf").
        WithContentType("application/pdf")
}
Redirect Response
func redirectHandler(r *http.Request) httphandler.Responder {
    return httphandler.Redirect("/new-location", http.StatusTemporaryRedirect).
        WithCookie(&http.Cookie{Name: "session", Value: "123"})
}
Plain Text Response
func healthCheckHandler(r *http.Request) httphandler.Responder {
    return plainresp.Success("OK").
        WithHeader("Cache-Control", "no-cache")
}
Response Customization

All responders support method chaining for customization:

return jsonresp.Success(data).
    WithStatus(http.StatusAccepted).
    WithHeader("X-Custom-Header", "value").
    WithCookie(&http.Cookie{Name: "session", Value: "123"}).
    WithLogger(logger)
Request Handling
JSON Request Parsing
func createUserHandler(r *http.Request, input CreateUserInput) httphandler.Responder {
    if err := input.Validate(); err != nil {
        return jsonresp.Error(err, "Invalid input", http.StatusBadRequest)
    }
    
    user, err := createUser(input)
    if err != nil {
        return jsonresp.InternalServerError(err)
    }

    return jsonresp.Success(user)
}

router.HandleFunc("POST /users", httphandler.HandleWithInput(createUserHandler))
Additional Examples

For more examples including a full REST API implementation see examples/main.go

Creating Custom Response Types

You can easily create your own response types by implementing the Responder interface.

Custom CSV Responder
// Define your custom responder
type CSVResponder struct {
    records    [][]string
    filename   string
    statusCode int
}

// Create a constructor
func NewCSVResponse(records [][]string, filename string) *CSVResponder {
    return &CSVResponder{
        records:    records,
        filename:   filename,
        statusCode: http.StatusOK,
    }
}

// Implement the Responder interface
func (res *CSVResponder) Respond(w http.ResponseWriter, r *http.Request) {
    // Set headers for CSV download
    w.Header().Set("Content-Type", "text/csv")
    w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, res.filename))
    
    // Write status code
    w.WriteHeader(res.statusCode)
    
    // Write CSV
    writer := csv.NewWriter(w)
    if err := writer.WriteAll(res.records); err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
}

// Usage example
func csvReportHandler(r *http.Request) httphandler.Responder {
    records := [][]string{
        {"Name", "Email", "Age"},
        {"John Doe", "john@example.com", "30"},
        {"Jane Doe", "jane@example.com", "28"},
    }
    return NewCSVResponse(records, "users.csv")
}

Benchmarks

Performance comparison between standard Go HTTP handlers and go-httphandler (benchmarked on Apple M3 Pro):

BenchmarkJSONResponse/Go/StandardHTTPHandler                      1145364      1051 ns/op      6118 B/op      18 allocs/op
BenchmarkJSONResponse/HTTPHandler/JSONResponse                    1000000      1121 ns/op      6295 B/op      21 allocs/op
BenchmarkJSONRequestResponse/Go/StandardHTTPHandlerWithInput      1000000      1291 ns/op      6275 B/op      22 allocs/op
BenchmarkJSONRequestResponse/HTTPHandler/JSONRequestResponse       961740      1257 ns/op      6379 B/op      26 allocs/op

Results show that go-httphandler adds a minimal and neglible overhead (~70 nanoseconds) while providing significant safety and maintainability benefits.

You can validate these results on your system by running:

go test -bench=. -benchmem

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrJSONDecode = errors.New("fail to decode json")

Functions

func Handle

func Handle(handler RequestHandler) http.HandlerFunc

Handle converts a RequestHandler to an http.HandlerFunc.

func HandleWithInput

func HandleWithInput[T any](handler RequestHandlerWithInput[T], opts ...func(*handleWithInput[T])) http.HandlerFunc

HandleWithInput converts a RequestHandlerWithInput to an http.HandlerFunc.

func JSONBodyDecode

func JSONBodyDecode[T any](r *http.Request) (T, error)

func Redirect

func Redirect(url string, code int) *redirectResponder

Redirect creates a new redirectResponder with the specified URL and status code.

func WithDecodeFunc

func WithDecodeFunc[T any](decodeFunc RequestDecodeFunc[T]) func(*handleWithInput[T])

WithDecodeFunc sets the decode function for the handler.

Types

type Logger

type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

type RequestDecodeFunc

type RequestDecodeFunc[T any] func(r *http.Request) (T, error)

RequestDecodeFunc defines how to decode an HTTP request.

type RequestHandler

type RequestHandler func(r *http.Request) Responder

RequestHandler handles an HTTP request and returns a Responder.

type RequestHandlerWithInput

type RequestHandlerWithInput[T any] func(r *http.Request, input T) Responder

RequestHandlerWithInput handles an HTTP request with decoded input and returns a Responder.

type Responder

type Responder interface {
	Respond(w http.ResponseWriter, r *http.Request)
}

Responder defines how to respond to HTTP requests.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL