Skip to content

struct0x/hx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Report Card Go Reference

hx

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.

Basic Usage

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)

Request Binding

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
}

Error Handling

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
}

Middleware

Apply middleware to all handlers:

hx := hx.New(
	hx.WithMiddlewares(loggingMiddleware, authMiddleware),
	hx.WithLogger(logger),
)

Testing

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

Response Types

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

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

Index

func Bind

func Bind[T any](r *http.Request, dst *T, opts ...BindOpt) error

Bind 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(err error, summary string, opts ...ProblemOpt) error

BindProblem 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(ctx context.Context) http.ResponseWriter

HijackResponseWriter 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.Opt

func WithMaxFormMemoryMB(maxFormMemoryMB int64) BindOpt

WithMaxFormMemoryMB 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(fn func(r *http.Request, name string) string) BindOpt

WithPathValueFunc 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(v *validator.Validate) BindOpt

WithValidator 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) Field

F 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) *HX

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

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

Middleware is a function that wraps a HandlerFunc.

type Middleware = alice.Constructor

type 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(mux Mux) Opt

WithCustomMux 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(log *slog.Logger) Opt

WithLogger 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(m ...Middleware) Opt

WithMiddlewares 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) Opt

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

ProblemDetails is a JSON object that describes an error. https://datatracker.ietf.org/doc/html/rfc9457

type ProblemDetails = out.ProblemDetails

func BadRequest(title string, opts ...ProblemOpt) ProblemDetails

BadRequest creates an HTTP response with a 400 (Bad Request) status code. It accepts a body of any type and optional response modifiers.

func Conflict(title string, opts ...ProblemOpt) ProblemDetails

Conflict creates an HTTP response with a 409 (Conflict) status code. It accepts a body of any type and optional response modifiers.

func Forbidden(title string, opts ...ProblemOpt) ProblemDetails

Forbidden creates an HTTP response with a 403 (Forbidden) status code. It accepts a body of any type and optional response modifiers.

func Internal(title string, opts ...ProblemOpt) ProblemDetails

Internal creates an HTTP response with a 500 (Internal Server Error) status code.

func MethodNotAllowed(title string, opts ...ProblemOpt) ProblemDetails

MethodNotAllowed 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(title string, opts ...ProblemOpt) ProblemDetails

NotAllowed 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(title string, opts ...ProblemOpt) ProblemDetails

NotFound 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) ProblemDetails

Problem creates a ProblemDetails instance with the provided status and summary.

func Unauthorized(title string, opts ...ProblemOpt) ProblemDetails

Unauthorized creates an HTTP response with a 401 (Unauthorized) status code. It accepts a body of any type and optional response modifiers.

type ProblemOpt interface {
    // contains filtered or unexported methods
}

func WithCause(err error) ProblemOpt

WithCause sets the underlying error that caused this problem. This error will be logged but not included in the JSON response.

func WithDetail(s string) ProblemOpt

WithDetail sets the Detail field of ProblemDetails. The detail contains a human-readable explanation specific to this occurrence of the problem.

func WithField(f Field) ProblemOpt

WithField adds a single field to the Extensions map of ProblemDetails. If Extensions is nil, it initializes a new map.

func WithFields(kv ...Field) ProblemOpt

WithFields sets multiple fields at once.

func WithInstance(s string) ProblemOpt

WithInstance sets the Instance field of ProblemDetails. The instance is a URI reference that identifies the specific occurrence of the problem.

func WithTypeURI(s string) ProblemOpt

WithTypeURI sets the Type field of ProblemDetails. The type is a URI reference that identifies the problem type.

Response representing an HTTP response with status, body, and headers.

type Response = out.Response

func Created

func Created(body any, opts ...ResponseOpt) *Response

Created 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) *Response

OK 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) *Response

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

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(ct string) ResponseOpt

WithContentType 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(c *http.Cookie) ResponseOpt

WithCookie 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(key, value string) ResponseOpt

WithHeader 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(headers http.Header) ResponseOpt

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

License

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

Contributing

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published