import "github.com/struct0x/hx"Package hx provides a lightweight HTTP framework for building RESTful APIs in Go. It focuses on simplifying request handling, response generation, and error management.
The framework implements RFC 9457 (Problem Details for HTTP APIs) for standardized error responses and provides a comprehensive request binding system that extracts data from multiple sources including query parameters, path variables, headers, cookies, JSON bodies, form data, and file uploads.
Create a new HX instance and register handlers:
hx := hx.New()
hx.Handle("/users", func(ctx context.Context, r *http.Request) error {
// Handle the request
return hx.OK(map[string]string{"message": "success"})
})
http.ListenAndServe(":8080", hx)Extract request data into structs using struct tags:
type UserRequest struct {
ID int `path:"id"`
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Auth string `header:"Authorization"`
Tags []string `query:"tags"`
}
func handler(ctx context.Context, r *http.Request) error {
var req UserRequest
if err := hx.Bind(r, &req); err != nil {
return hx.BindProblem(err, "Invalid request")
}
// Use req...
return nil
}Return structured errors using ProblemDetails:
func handler(ctx context.Context, r *http.Request) error {
if !authorized(r) {
return hx.Unauthorized("Access denied",
hx.WithDetail("Invalid authentication token"),
hx.WithTypeURI("https://example.com/errors/auth"))
}
return nil
}Apply middleware to all handlers:
hx := hx.New(
hx.WithMiddlewares(loggingMiddleware, authMiddleware),
hx.WithLogger(logger),
)Test handlers using the hxtest package:
func TestHandler(t *testing.T) {
hxtest.Test(t, handler).
Do(httptest.NewRequest("GET", "/test", nil)).
Expect(hxtest.Status(http.StatusOK)).
Expect(hxtest.Body(map[string]string{"message": "success"}))
}The framework supports multiple response types:
- hx.OK(body): 200 OK with JSON body
- hx.Created(body): 201 Created with JSON body
- hx.Problem(status, title): RFC 9457 problem details
- hx.Respond(status, body, opts...): Custom response with options
Configure the HX instance with functional options:
- WithLogger: Set a custom logger
- WithCustomMux: Use a custom http.Handler
- WithMiddlewares: Apply middleware functions
- WithProblemInstanceGetter: Set a function to generate problem instance URIs
For more examples, see the example package.
- func Bind[T any](r *http.Request, dst *T, opts ...BindOpt) error
- func BindProblem(err error, summary string, opts ...ProblemOpt) error
- func HijackResponseWriter(ctx context.Context) http.ResponseWriter
- type BindOpt
- type Field
- type HX
- type HandlerFunc
- type Middleware
- type Mux
- type Opt
- type ProblemDetails
- func BadRequest(title string, opts ...ProblemOpt) ProblemDetails
- func Conflict(title string, opts ...ProblemOpt) ProblemDetails
- func Forbidden(title string, opts ...ProblemOpt) ProblemDetails
- func Internal(title string, opts ...ProblemOpt) ProblemDetails
- func MethodNotAllowed(title string, opts ...ProblemOpt) ProblemDetails
- func NotAllowed(title string, opts ...ProblemOpt) ProblemDetails
- func NotFound(title string, opts ...ProblemOpt) ProblemDetails
- func Problem(status int, summary string, opts ...ProblemOpt) ProblemDetails
- func Unauthorized(title string, opts ...ProblemOpt) ProblemDetails
- type ProblemOpt
- type Response
- type ResponseOpt
func Bind
func Bind[T any](r *http.Request, dst *T, opts ...BindOpt) errorBind extracts data from an HTTP request into a destination struct. It supports binding from multiple sources including URL query parameters, path variables, headers, cookies, JSON body, form data, and multipart file uploads.
The destination must be a pointer to a struct. Fields in the struct are bound based on struct tags that specify the data source and field name:
- `query:"name"` - binds from URL query parameters
- `path:"name"` - binds from URL path variables
- `header:"Name"` - binds from HTTP headers
- `cookie:"name"` - binds from HTTP cookies
- `json:"name"` - binds from JSON request body (application/json)
- `form:"name"` - binds from form data (application/x-www-form-urlencoded or multipart/form-data)
- `file:"name"` - binds file uploads from multipart/form-data
Supported field types include:
- Basic types: string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64
- Slices of basic types (for multiple values)
- Slices of strings (for headers and query parameters with multiple values)
- http.Cookie (for cookie fields)
- multipart.FileHeader (for single file uploads)
- []*multipart.FileHeader (for multiple file uploads)
- Types implementing encoding.TextUnmarshaler
- Any type for json-tagged fields (unmarshaled via encoding/json)
Options:
- WithPathValueFunc: provides a custom function to extract path variables from the request. By default, uses http.Request.PathValue.
- WithMaxFormMemoryMB: sets the maximum memory in megabytes for parsing multipart forms. Defaults to 32 MB if not specified.
- WithValidator: sets a custom validator.Validate instance to be used for request validation. By default, a default validator is used.
Returns:
- nil if all fields are successfully bound
- error if any field fails to bind, use BindProblem to create a structured error response
Example usage:
type UserRequest struct {
ID int `path:"id"`
Name string `json:"name"`
Email string `json:"email"`
Tags []string `query:"tags"`
AuthToken string `header:"Authorization"`
}
func handler(ctx context.Context, r *http.Request) error {
var req UserRequest
if err := hx.Bind(r, &req); err != nil {
return BindProblem(err, "Invalid user request",
WithTypeURI("https://example.com/errors/invalid-user"))
}
// Use req...
return nil
}
func BindProblem
func BindProblem(err error, summary string, opts ...ProblemOpt) errorBindProblem creates a structured error response by wrapping binding errors into a ProblemDetails. It takes an error from Bind, a summary message, and optional ProblemOpt options.
The function handles different types of binding errors:
- Structural errors (nil request, nil destination, invalid types) return 500 Internal Server Error
- Validation errors return 400 Bad Request with detailed field errors in the extensions
- Other errors return 500 Internal Server Error
For validation errors, the response includes an "errors" field in the extensions containing an array of objects with "field" and "detail" properties for each validation error.
Allowed ProblemOpt options: - WithTypeURI sets the Type field of ProblemDetails. - WithDetail sets the Detail field of ProblemDetails. - WithField adds a single field to the Extensions map of ProblemDetails. - WithFields sets multiple fields at once. - WithInstance sets the Instance field of ProblemDetails. Note: WithCause option is automatically added and will be ignored if provided manually.
Example usage:
var req UserRequest
if err := Bind(r, &req); err != nil {
return BindProblem(err, "Invalid user request",
WithTypeURI("https://example.com/errors/invalid-user"))
}
func HijackResponseWriter
func HijackResponseWriter(ctx context.Context) http.ResponseWriterHijackResponseWriter retrieves the http.ResponseWriter from the context. When the ResponseWriter is hijacked, the return value from HandlerFunc will be ignored.
type BindOpt
type BindOpt = bind.Optfunc WithMaxFormMemoryMB
func WithMaxFormMemoryMB(maxFormMemoryMB int64) BindOptWithMaxFormMemoryMB configures the maximum size of multipart form data that will reside in memory. The rest of the data will be stored on disk in temporary files. This option is used when binding multipart form data and file uploads.
func WithPathValueFunc
func WithPathValueFunc(fn func(r *http.Request, name string) string) BindOptWithPathValueFunc overrides the default way of extracting a path parameter from the request. The function receives the request and the name of the path variable and must return the value (or the empty string if the variable is not present).
func WithValidator
func WithValidator(v *validator.Validate) BindOptWithValidator configures a custom validator.Validate instance to be used for request validation. The validator will be used to validate struct fields with "validate" tags after binding. If not provided, a default validator will be used.
type Field
Field represents a key-value pair that can be added to ProblemDetails extensions.
type Field struct {
Key string
Val any
}func F
func F(k string, v any) FieldF is a shorthand constructor for creating Field instances.
type HX
HX is a framework for building HTTP APIs with enhanced error handling and middleware support. It provides a convenient way to handle HTTP requests, manage middleware chains, and standardize error responses using ProblemDetails (RFC 9457).
Example usage:
hx := hx.New(
hx.WithLogger(slog.Default()),
hx.WithMux(http.NewServeMux()),
hx.WithMiddleware(loggingMiddleware),
)
// Handle requests
hx.Handle("/api/users", func(ctx context.Context, r *http.Request) error {
// Handle the request
return nil
})
// Start the server
http.ListenAndServe(":8080", hx)type HX struct {
// contains filtered or unexported fields
}func New
func New(opts ...Opt) *HXNew creates a new HX instance.
func (*HX) Handle
func (h *HX) Handle(pattern string, handler HandlerFunc, mids ...Middleware)Handle registers a new request handler with the given pattern and middleware.
func (*HX) ServeHTTP
func (h *HX) ServeHTTP(w http.ResponseWriter, r *http.Request)type HandlerFunc
HandlerFunc is a function type that handles HTTP requests in HX framework. It receives a context.Context and *http.Request as input parameters and returns an error. Context is identical to http.Request.Context, but it includes a ResponseWriter that can be hijacked.
If HandlerFunc returns: - nil: the response will be 204 No Content - ProblemDetails: the response will be encoded as application/problem+json - Response: the response will be encoded as application/json with custom headers - any other error: the response will be 500 Internal Server Error
Example usage:
hx.HandlerFunc(func(ctx context.Context, r *http.Request) error {
// Handle the request
return nil // or return an error
})type HandlerFunc func(ctx context.Context, r *http.Request) errortype Middleware
Middleware is a function that wraps a HandlerFunc.
type Middleware = alice.Constructortype Mux
Mux is an interface that wraps the http.Handler.
type Mux interface {
http.Handler
Handle(pattern string, handler http.Handler)
}type Opt
type Opt interface {
// contains filtered or unexported methods
}func WithCustomMux
func WithCustomMux(mux Mux) OptWithCustomMux sets a custom multiplexer for the HX instance. The provided mux will be used for routing HTTP requests. If not set, http.DefaultServeMux will be used.
func WithLogger
func WithLogger(log *slog.Logger) OptWithLogger sets a custom logger for the HX instance. The provided logger will be used for error logging and debugging purposes. If not set, slog.Default() will be used.
func WithMiddlewares
func WithMiddlewares(m ...Middleware) OptWithMiddlewares sets middleware functions for the HX instance. These middlewares will be applied to all handlers in the order they are provided. Each middleware should implement the Middleware interface.
func WithProblemInstanceGetter(f func(ctx context.Context) string) OptWithProblemInstanceGetter sets a function that provides the "instance" value for ProblemDetails. This is particularly useful in distributed tracing scenarios or when using error tracking systems like Sentry, as it allows linking specific error instances to their corresponding traces or external error reports. The provided function receives a context and should return a string identifier that uniquely represents this error occurrence.
type ProblemDetails
ProblemDetails is a JSON object that describes an error. https://datatracker.ietf.org/doc/html/rfc9457
type ProblemDetails = out.ProblemDetailsfunc BadRequest
func BadRequest(title string, opts ...ProblemOpt) ProblemDetailsBadRequest creates an HTTP response with a 400 (Bad Request) status code. It accepts a body of any type and optional response modifiers.
func Conflict
func Conflict(title string, opts ...ProblemOpt) ProblemDetailsConflict creates an HTTP response with a 409 (Conflict) status code. It accepts a body of any type and optional response modifiers.
func Forbidden
func Forbidden(title string, opts ...ProblemOpt) ProblemDetailsForbidden creates an HTTP response with a 403 (Forbidden) status code. It accepts a body of any type and optional response modifiers.
func Internal
func Internal(title string, opts ...ProblemOpt) ProblemDetailsInternal creates an HTTP response with a 500 (Internal Server Error) status code.
func MethodNotAllowed
func MethodNotAllowed(title string, opts ...ProblemOpt) ProblemDetailsMethodNotAllowed creates an HTTP response with a 405 (Method Not Allowed) status code. It accepts a body of any type and optional response modifiers.
func NotAllowed
func NotAllowed(title string, opts ...ProblemOpt) ProblemDetailsNotAllowed creates an HTTP response with a 405 (Method Not Allowed) status code. It accepts a title string describing the error and optional response modifiers. Returns a ProblemDetails object that represents the error response.
func NotFound
func NotFound(title string, opts ...ProblemOpt) ProblemDetailsNotFound creates an HTTP response with a 404 (Not Found) status code. It accepts a body of any type and optional response modifiers.
func Problem
func Problem(status int, summary string, opts ...ProblemOpt) ProblemDetailsProblem creates a ProblemDetails instance with the provided status and summary.
func Unauthorized
func Unauthorized(title string, opts ...ProblemOpt) ProblemDetailsUnauthorized creates an HTTP response with a 401 (Unauthorized) status code. It accepts a body of any type and optional response modifiers.
type ProblemOpt
type ProblemOpt interface {
// contains filtered or unexported methods
}func WithCause
func WithCause(err error) ProblemOptWithCause sets the underlying error that caused this problem. This error will be logged but not included in the JSON response.
func WithDetail
func WithDetail(s string) ProblemOptWithDetail sets the Detail field of ProblemDetails. The detail contains a human-readable explanation specific to this occurrence of the problem.
func WithField
func WithField(f Field) ProblemOptWithField adds a single field to the Extensions map of ProblemDetails. If Extensions is nil, it initializes a new map.
func WithFields
func WithFields(kv ...Field) ProblemOptWithFields sets multiple fields at once.
func WithInstance
func WithInstance(s string) ProblemOptWithInstance sets the Instance field of ProblemDetails. The instance is a URI reference that identifies the specific occurrence of the problem.
func WithTypeURI
func WithTypeURI(s string) ProblemOptWithTypeURI sets the Type field of ProblemDetails. The type is a URI reference that identifies the problem type.
type Response
Response representing an HTTP response with status, body, and headers.
type Response = out.Responsefunc Created
func Created(body any, opts ...ResponseOpt) *ResponseCreated creates an HTTP response with a 201 (Created) status code. It accepts a body of any type and optional response modifiers.
func OK
func OK(body any, opts ...ResponseOpt) *ResponseOK creates a successful HTTP response with a 200 (OK) status code. It accepts a body of any type and optional response modifiers.
func Respond
func Respond(status int, body any, opts ...ResponseOpt) *ResponseRespond creates an HTTP response with the specified status code and body. It accepts a status code, a body of any type, and optional response modifiers.
type ResponseOpt
ResponseOpt is an interface for options that can modify both Response and ProblemDetails objects. It provides methods to apply modifications to these types, allowing for flexible configuration of HTTP responses. Implementations of this interface can modify headers, cookies, and other response attributes consistently across both normal responses and problem details.
type ResponseOpt interface {
// contains filtered or unexported methods
}func WithContentType
func WithContentType(ct string) ResponseOptWithContentType sets the Content-Type header for the response. The provided content type string will be used as the value for the Content-Type header. This option only affects Response objects and has no effect on ProblemDetails. For ProblemDetails, the Content-Type is always set to "application/problem+json".
func WithCookie
func WithCookie(c *http.Cookie) ResponseOptWithCookie adds an HTTP cookie to the response. It accepts a pointer to an http.Cookie and returns a ResponseOpt that can be used to modify a Response object. The provided cookie will be included in the final HTTP response.
func WithHeader
func WithHeader(key, value string) ResponseOptWithHeader sets a single HTTP header for the response. It accepts a key-value pair representing the header name and value, and returns a ResponseOpt that can be used to modify a Response object. The provided header will be added to the final HTTP response.
func WithHeaders
func WithHeaders(headers http.Header) ResponseOptWithHeaders sets custom HTTP headers for the response. It accepts an http.Header map and returns a ResponseOpt that can be used to modify a Response object. The provided headers will be used in the final HTTP response.
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.