httpx

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2026 License: MIT Imports: 17 Imported by: 0

README

HTTPX

httpx is a small helper library to work with Go's standard net/http package. The package is designed to be simple yet powerful, providing just the essential features needed for building HTTP APIs without the complexity of larger frameworks.

The main asset of httpx is its error-returning handlers and middleware pattern. Instead of handling errors within each handler, errors are returned and managed centrally by a single errorHandler function. This dramatically reduces boilerplate error handling code in your handlers.

The router and handler groups can be used standalone with the standard http.ListenAndServe or with the provided httpx.ListenAndServe function which includes graceful shutdown support.

Key Features:
  • Error-returning handlers/middlewares: returns errors instead of handling them inline
  • Centralized error handling: All errors managed by a single errorHandler function
  • Route grouping: Organize routes with prefixes and shared middleware
  • Middleware chain: Composable middleware with Next() pattern
  • Graceful shutdown: Optional ListenAndServe with built-in signal handling
Basic Usage
import "https://github.com/quentinalbertone/httpx"

func main() {
    router := httpx.NewRouter(nil, nil)
    router.GET("/hello", func(w http.ResponseWriter, r *http.Request) error {
        return httpx.JSON(w, http.StatusOK, map[string]string{"message": "Hello, World!"})
    })
    http.ListenAndServe(":8080", router)
}
With Middleware
func AuthMiddleware(w http.ResponseWriter, r *http.Request) error {
    // Check authentication
    if !isAuthenticated(r) {
        return httpx.JSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
    }
    return httpx.Next(w, r) // Continue to next handler
}

func main() {
	router := httpx.NewRouter(nil, nil)
	router.GET("/protected", AuthMiddleware, func(w http.ResponseWriter, r *http.Request) error {
		return httpx.JSON(w, http.StatusOK, map[string]string{"data": "secret"})
	})
}
Route Groups
func main() {
    router := httpx.NewRouter(nil, nil)
    api := router.Group("/api")
    {
        v1 := api.Group("/v1")

        v1.GET("/users", GetUsers)
        v1.POST("/users", CreateUser)
        v1.PUT("/users/{id}", UpdateUser)
    }
}
Error Handling
func MyErrorHandler(fn httpx.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if err := fn(w, r); err != nil {
            // Custom error handling logic
            var status int
            var msg string
            switch {
            case errors.Is(err, customerr.ValidationErr):
                status = http.StatusBadRequest
                msg = err.Error()
            case errors.Is(err, customerr.NotFoundErr):
                status = http.StatusNotFound
                msg = err.Error()
            case errors.Is(err, ierrors.NotImplementedErr):
                status = http.StatusNotImplemented
                msg = "not implemented"
            case errors.Is(err, ierrors.UnauthorizedErr):
                status = http.StatusUnauthorized
                msg = "not authorized"
            default:
                status = http.StatusInternalServerError
                err = errors.New("internal server error")
            }
            httpx.JSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
        }
    }
}

func main() {
    router := httpx.NewRouter(nil, MyErrorHandler)
}
Using with Standard Library
func main() {
    router := httpx.NewRouter(nil, nil)
    router.GET("/api/data", GetDataHandler)
    http.ListenAndServe(":8080", router)
}
Using with Graceful Shutdown
func main() {
    router := httpx.NewRouter(nil, nil)
    router.GET("/api/data", GetDataHandler)

    serverConf := func(server *http.Server) {
        server.Addr = ":8080"
        server.ReadTimeout = 5 * time.Second
        server.WriteTimeout = 10 * time.Second
    }

    ctx := context.Background()
    if err := httpx.ListenAndServe(ctx, router, serverConf); err != nil {
        log.Fatal(err)
    }
}

The package is designed to be minimal and focused: it's a small helper that makes working with Go's net/http package more convenient by centralizing error handling and providing useful routing utilities.

Documentation

Overview

httpx is a small helper library to work with Go's standard net/http package. The package is designed to be simple yet powerful, providing just the essential features needed for building HTTP APIs without the complexity of larger frameworks.

The main asset of httpx is its error-returning handlers and middleware pattern. Instead of handling errors within each handler, errors are returned and managed centrally by a single errorHandler function. This dramatically reduces boilerplate error handling code in your handlers.

The router and handler groups can be used standalone with the standard http.ListenAndServe or with the provided httpx.ListenAndServe function which includes graceful shutdown support.

Key Features:

  • Error-returning handlers/middlewares: returns errors instead of handling them inline
  • Centralized error handling: All errors managed by a single errorHandler function
  • Route grouping: Organize routes with prefixes and shared middleware
  • Middleware chain: Composable middleware with Next() pattern
  • Graceful shutdown: Optional ListenAndServe with built-in signal handling

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// IsShuttingDown is an atomic boolean flag indicating whether the server
	// is currently in the shutdown process. This can be used by readiness
	// checks to fail health checks during graceful shutdown.
	IsShuttingDown atomic.Bool

	// ErrServerHasStopped is returned when the server has initiated shutdown
	// and is no longer accepting new requests.
	ErrServerHasStopped = errors.New("server has stopped")
)
View Source
var (
	ErrNotStruct       = errors.New("out is not a struct")
	ErrNotPointer      = errors.New("out must be a pointer")
	ErrUnsupportedType = errors.New("unsupported type")
)

Functions

func DecodeQueryParams

func DecodeQueryParams(uri url.URL, out any) error

DecodeQueryParams parses URL query parameters into a struct based on field tags.

The function uses struct field tags with the key "query" to map query parameters to struct fields. It supports:

  • Basic types: string, int, uint, bool, float
  • Slices of basic types (e.g., ?ids=1&ids=2&ids=3)
  • Nested structs using dot notation (e.g., ?address.city=Paris)
  • Custom types implementing encoding.TextUnmarshaler

Tag usage:

  • `query:"param_name"` - maps to the specified query parameter
  • `query:"-"` - ignores the field
  • no tag or empty tag - uses the field name as the parameter name

The out parameter must be a pointer to a struct

Example (NestedStruct)

ExampleDecodeQueryParams_nestedStruct demonstrates using DecodeQueryParams with nested structs.

package main

import (
	"fmt"
	"net/url"

	"github.com/quentinalbertone/httpx"
)

func main() {
	// Define structs with nested structure
	type Location struct {
		City    string `query:"city"`
		Country string `query:"country"`
	}

	type User struct {
		Name     string   `query:"name"`
		Age      int      `query:"age"`
		Location Location `query:"loc"`
	}

	// Parse a URL with nested query parameters
	u, _ := url.Parse("https://api.example.com/users?name=Alice&age=30&loc.city=Paris&loc.country=France")

	// Decode query parameters into the struct
	var filter User
	err := httpx.DecodeQueryParams(*u, &filter)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Use the decoded values
	fmt.Printf("Name: %s\n", filter.Name)
	fmt.Printf("Age: %d\n", filter.Age)
	fmt.Printf("City: %s\n", filter.Location.City)
	fmt.Printf("Country: %s\n", filter.Location.Country)

}
Output:

Name: Alice
Age: 30
City: Paris
Country: France

func JSON

func JSON(w http.ResponseWriter, status int, body any) error

JSON writes a JSON response with the given status code and body. It sets the Content-Type header to "application/json" and encodes the body using json.Encoder. Returns an error if the JSON encoding fails.

Example:

func GetUser(w http.ResponseWriter, r *http.Request) error {
    user := User{ID: 1, Name: "John"}
    return httpx.JSON(w, http.StatusOK, user)
}
Example

ExampleJSON demonstrates the JSON utility function.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/quentinalbertone/httpx"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) error {
		data := struct {
			Message string `json:"message"`
			Count   int    `json:"count"`
		}{
			Message: "Hello from JSON",
			Count:   42,
		}
		return httpx.JSON(w, http.StatusOK, data)
	}

	// Create a test request
	req := httptest.NewRequest("GET", "/test", nil)
	w := httptest.NewRecorder()

	// Call the handler
	err := handler(w, req)
	if err != nil {
		panic(err)
	}

	resp := w.Result()
	fmt.Println("Status:", resp.StatusCode)
	fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
}
Output:

Status: 200
Content-Type: application/json

func ListenAndServe

func ListenAndServe(ctx context.Context, router http.Handler, serverConfs ...ServerConfFn) error

ListenAndServe starts an HTTP server with graceful shutdown capabilities. It listens for SIGINT and SIGTERM signals to initiate a graceful shutdown sequence.

The shutdown sequence follows these steps:

  1. Upon receiving a shutdown signal or when the given context is done, sets IsShuttingDown to true
  2. Waits readinessDrainDelay for readiness checks to propagate
  3. Stops accepting new connections and waits up to shutdownPeriod for ongoing requests
  4. If graceful shutdown fails, waits an additional shutdownHardPeriod before returning

Returns an error if the server fails to start or encounters an error during shutdown.

Example

ExampleListenAndServe demonstrates how to start an HTTP server with graceful shutdown and a CORS middleware applied globally via WithHTTPMiddleware.

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/quentinalbertone/httpx"
)

func main() {

	// Create a router with routes
	router := httpx.NewRouter(nil, nil)

	router.GET("/api/users", func(w http.ResponseWriter, r *http.Request) error {
		return httpx.JSON(w, http.StatusOK, []string{"Alice", "Bob"})
	})

	// CORSMiddleware sets CORS headers on every response and handles preflight requests.
	corsMiddleware := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

			// Handle preflight requests
			if r.Method == http.MethodOptions {
				w.WriteHeader(http.StatusOK)
				return
			}

			next.ServeHTTP(w, r)
		})
	}

	// Configure the server with custom settings
	serverConf := func(server *http.Server) {
		server.Addr = ":8080"
		// You can set other server options here:
		// server.ReadTimeout = 5 * time.Second
		// server.WriteTimeout = 10 * time.Second
		// server.IdleTimeout = 120 * time.Second
	}

	// Start the server with CORS enabled globally.
	// WithHTTPMiddleware wraps the server's handler so it applies to all
	// requests, including those that don't match any route.
	ctx := context.Background()
	if err := httpx.ListenAndServe(ctx, router,
		serverConf,
		httpx.WithHTTPMiddleware(corsMiddleware),
	); err != nil {
		fmt.Printf("Server error: %v\n", err)
	}
}

func Next

func Next(w http.ResponseWriter, r *http.Request) error

Next calls the next handler in the middleware chain. Middleware should call Next to continue the request processing. If there is no next handler, Next returns nil.

Example:

func MyMiddleware(w http.ResponseWriter, r *http.Request) error {
    // Do something before
    err := httpx.Next(w, r)
    // Do something after
    return err
}
Example

ExampleNext demonstrates middleware usage with the Next function.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/quentinalbertone/httpx"
)

func main() {
	// Custom middleware that adds a header
	headerMiddleware := func(w http.ResponseWriter, r *http.Request) error {
		w.Header().Set("X-Custom-Header", "middleware-value")
		// Call the next handler in the chain
		return httpx.Next(w, r)
	}

	router := httpx.NewRouter(nil, nil)

	// Add a route with middleware
	router.GET("/protected", headerMiddleware, func(w http.ResponseWriter, r *http.Request) error {
		return httpx.JSON(w, http.StatusOK, map[string]string{
			"message": "This response has a custom header",
		})
	})

	server := httptest.NewServer(router)
	defer server.Close()

	resp, err := http.Get(server.URL + "/protected")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	fmt.Println("Custom Header:", resp.Header.Get("X-Custom-Header"))
}
Output:

Custom Header: middleware-value

Types

type ErrorHandlerFunc

type ErrorHandlerFunc func(HandlerFunc) http.HandlerFunc

ErrorHandlerFunc converts a HandlerFunc (which returns an error) into a standard http.HandlerFunc. It is responsible for handling any errors returned by the HandlerFunc and writing appropriate HTTP responses.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group represents a router group that can have a prefix, middlewares, and sub-groups. It implements http.Handler and can be used directly with http.Server.

Example

ExampleGroup demonstrates route grouping and middleware.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/quentinalbertone/httpx"
)

func main() {
	router := httpx.NewRouter(nil, nil)

	// Create an API group with a common prefix
	api := router.Group("/api")

	// Create a v1 subgroup
	v1 := api.Group("/v1")

	// Add routes to the v1 group
	v1.GET("/users", func(w http.ResponseWriter, r *http.Request) error {
		users := []map[string]interface{}{
			{"id": 1, "name": "Alice"},
			{"id": 2, "name": "Bob"},
		}
		return httpx.JSON(w, http.StatusOK, users)
	})

	v1.POST("/users", func(w http.ResponseWriter, r *http.Request) error {
		return httpx.JSON(w, http.StatusCreated, map[string]string{
			"message": "User created",
		})
	})

	server := httptest.NewServer(router)
	defer server.Close()

	// Test the grouped route
	resp, err := http.Get(server.URL + "/api/v1/users")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	fmt.Println("Status:", resp.StatusCode)
}
Output:

Status: 200

func NewRouter

func NewRouter(logger LoggingFunc, errorHandler ErrorHandlerFunc, ms ...HandlerFunc) *Group

NewRouter creates a new root router with optional global middlewares and logger. The logger wraps all routes for logging HTTP requests and responses. The errorHandler converts HandlerFunc errors into HTTP responses.

Example

ExampleNewRouter demonstrates basic usage of the httpx router.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/quentinalbertone/httpx"
)

func main() {
	// Create a new router with default logging and error handling
	router := httpx.NewRouter(nil, nil)

	// Add a simple GET route
	router.GET("/hello", func(w http.ResponseWriter, r *http.Request) error {
		return httpx.JSON(w, http.StatusOK, map[string]string{
			"message": "Hello, World!",
		})
	})

	// Create a test server
	server := httptest.NewServer(router)
	defer server.Close()

	// Make a request to test the route
	resp, err := http.Get(server.URL + "/hello")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	fmt.Println("Status:", resp.StatusCode)
	fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
}
Output:

Status: 200
Content-Type: application/json

func (*Group) CONNECT

func (g *Group) CONNECT(path string, handlers ...HandlerFunc)

CONNECT registers a CONNECT route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) DELETE

func (g *Group) DELETE(path string, handlers ...HandlerFunc)

DELETE registers a DELETE route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) GET

func (g *Group) GET(path string, handlers ...HandlerFunc)

GET registers a GET route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) Group

func (g *Group) Group(prefix string, ms ...HandlerFunc) *Group

Group creates a new subgroup with a prefix and optional middlewares. The subgroup inherits the parent's mux, logger, and errorHandler. Middlewares from the parent group are combined with the new middlewares.

func (*Group) HEAD

func (g *Group) HEAD(path string, handlers ...HandlerFunc)

HEAD registers a HEAD route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) OPTIONS

func (g *Group) OPTIONS(path string, handlers ...HandlerFunc)

OPTIONS registers a OPTIONS route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) PATCH

func (g *Group) PATCH(path string, handlers ...HandlerFunc)

PATCH registers a PATCH route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) POST

func (g *Group) POST(path string, handlers ...HandlerFunc)

POST registers a POST route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) PUT

func (g *Group) PUT(path string, handlers ...HandlerFunc)

PUT registers a PUT route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

func (*Group) ServeHTTP

func (g *Group) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface. This allows Group to be used directly with http.Server.

Example:

router := httpx.NewRouter(nil, nil)
http.ListenAndServe(":8080", router)

func (*Group) TRACE

func (g *Group) TRACE(path string, handlers ...HandlerFunc)

TRACE registers a TRACE route with optional middlewares and a final handler. Middlewares are executed in the order provided, followed by the final handler.

type HTTPMiddleware

type HTTPMiddleware func(http.Handler) http.Handler

HTTPMiddleware is a standard HTTP middleware that wraps an http.Handler. Unlike HandlerFunc middleware which is part of the route chain and returns errors, HTTPMiddleware runs before routing and follows the standard middleware pattern.

type HandlerFunc

type HandlerFunc func(w http.ResponseWriter, r *http.Request) error

HandlerFunc is an HTTP handler that returns an error. This allows handlers to return errors which can be handled centrally by an ErrorHandlerFunc.

type LoggingFunc

type LoggingFunc func(http.Handler) http.Handler

LoggingFunc is a function that wraps an http.Handler to add logging functionality. It follows the standard middleware pattern for http.Handler.

type ServerConfFn

type ServerConfFn func(*http.Server)

ServerConfFn is a function type used to configure an http.Server instance before starting it. This allows callers to customize server settings such as timeouts, TLS configuration, and other http.Server fields.

func WithHTTPMiddleware

func WithHTTPMiddleware(middlewares ...HTTPMiddleware) ServerConfFn

WithHTTPMiddleware returns a ServerConfFn that wraps the server's handler with the given HTTP middlewares. Middlewares are applied so that they execute in the order they are provided (first middleware runs first). This is useful for functionality like CORS, request ID generation, or metrics that should apply to all requests including those that don't match any route.

Example:

httpx.ListenAndServe(ctx, router,
    httpx.WithHTTPMiddleware(CORSMiddleware, RequestIDMiddleware),
)

Directories

Path Synopsis
example
example-1 command
listenAndServe command

Jump to

Keyboard shortcuts

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