chain

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 6, 2025 License: MIT Imports: 2 Imported by: 0

README

Chain

A lightweight, flexible HTTP middleware chaining solution for Go

Overview

Chain is a composable HTTP middleware router package that provides a chainable API for organizing your web application's routes and middleware. It is built on top of Go's standard http.ServeMux.

Chain is designed to work with Go 1.22's new routing enhancements, supporting HTTP method matching and path wildcards in the same pattern format as the standard library. This makes it easy to transition between standard Go HTTP servers and Chain's enhanced middleware capabilities.

Chain was inspired by alexedwards/flow.

Features

  • Middleware Chaining: Easily add request/response processing middleware
  • Method Chaining API: API for registering routes and middleware
  • Response Monitoring: Optional response wrapper for tracking status codes and sizes
  • Route Grouping: Group routes with their own isolated middleware stacks
  • Custom Error Handlers: Define custom handlers for 404 Not Found and 405 Method Not Allowed responses
  • Go 1.22 Compatible: Works with Go's new routing enhancements including method matching and path wildcards

Installation

go get github.com/caelisco/chain

Basic Usage

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/caelisco/chain"
)

func main() {
	// Create a new Chain router
	mux := chain.New()

	// Add global logging middleware
	mux.Use(loggingMiddleware)

	// Add routes
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Welcome to the home page!")
	})

	// Add authenticated routes in a group
	mux.Group(func(api *chain.Mux) {
		// This middleware only applies to routes in this group
		api.Use(authMiddleware)

		api.HandleFunc("GET /dashboard", func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintf(w, "Welcome to your dashboard!")
		})
	})

	// Start the server
	log.Println("Server starting on :8080")
	http.ListenAndServe(":8080", mux)
}

// Example logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("Started %s %s", r.Method, r.URL.Path)
		
		next.ServeHTTP(w, r)
		
		log.Printf("Completed in %v", time.Since(start))
	})
}

// Example auth middleware
func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check authentication (simplified for example)
		if r.Header.Get("X-API-Key") == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		
		next.ServeHTTP(w, r)
	})
}

Chaining API

Chain provides a fluent method chaining API for a more expressive syntax:

mux := chain.New().
    Use(loggingMiddleware).
    Use(recoverMiddleware).
    HandleFunc("GET /", homeHandler).
    HandleFunc("POST /users", createUserHandler).
    HandleFunc("GET /users/{id}", getUserHandler)

Note that Chain follows Go 1.22's pattern matching rules, including method matching and path wildcards. You can access path parameters using r.PathValue("id") in your handlers.

Path Wildcards and Parameters

Chain uses Go 1.22's path parameter syntax. Access path parameters in your handlers using r.PathValue():

// Match a specific path segment with a named parameter
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    // Get the id parameter value
    id := r.PathValue("id")
    fmt.Fprintf(w, "User ID: %s", id)
})

// Use trailing ... to capture all remaining path segments
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    fmt.Fprintf(w, "Requested file path: %s", path)
})

// The {$} pattern to match exact paths with trailing slashes
mux.HandleFunc("GET /posts/{$}", listPostsHandler)      // Matches ONLY "/posts/" exactly
mux.HandleFunc("GET /posts/{id}", getSpecificPostHandler) // Matches "/posts/123", etc.

// Routes with trailing slashes (like /posts/) match all paths with that prefix
// but {$} restricts to exact match
mux.HandleFunc("GET /api/", apiIndexHandler)           // Matches "/api/", "/api/users", etc.
mux.HandleFunc("GET /api/{$}", apiExactHandler)        // Matches ONLY "/api/" exactly

The {$} pattern is particularly useful when you need to distinguish between a collection endpoint (e.g., /posts/) and a specific resource endpoint (e.g., /posts/{id}).

Response Wrapper

Enable the response wrapper to track status codes, response size, and more detailed logging:

mux := chain.New().
	WithResponseWrapper().
	Use(advancedLoggingMiddleware)

// Advanced logging middleware with response information
func advancedLoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		
		next.ServeHTTP(w, r)
		
		// Access response information after handler execution
		if rw, ok := w.(chain.ResponseWriter); ok {
			log.Printf(
				"%s %s %d %d %v",
				r.Method,
				r.URL.Path,
				rw.Status(),
				rw.Size(),
				time.Since(start),
			)
		}
	})
}

Custom Error Handlers

Set custom handlers for 404 Not Found and 405 Method Not Allowed responses. The response wrapper is automatically enabled when using these handlers:

// Using named handler functions
mux := chain.New().
	WithNotFound(http.HandlerFunc(customNotFoundHandler)).
	WithMethodNotAllowed(http.HandlerFunc(customMethodNotAllowedHandler))

func customNotFoundHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusNotFound)
	fmt.Fprintf(w, "Sorry, the page %s was not found", r.URL.Path)
}

func customMethodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusMethodNotAllowed)
	fmt.Fprintf(w, "Method %s is not allowed for %s", r.Method, r.URL.Path)
}

// Using inline anonymous functions
mux := chain.New().
	WithNotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, "Sorry, the page %s was not found", r.URL.Path)
	})).
	WithMethodNotAllowed(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusMethodNotAllowed)
		fmt.Fprintf(w, "Method %s is not allowed for %s", r.Method, r.URL.Path)
	}))

Route Grouping

Group related routes and apply middleware only to those routes:

// API routes with authentication
mux.Group(func(api *chain.Mux) {
    // Auth middleware only applies to these routes
    api.Use(authMiddleware)
    
    api.HandleFunc("GET /api/users", listUsersHandler)
    api.HandleFunc("POST /api/users", createUserHandler)
    
    // Nested group for admin-only routes
    api.Group(func(admin *chain.Mux) {
        admin.Use(adminOnlyMiddleware)
        
        api.HandleFunc("DELETE /api/users/{id}", deleteUserHandler)
    })
})

Groups are useful for organizing routes by functionality, applying specific middleware to sets of routes, and creating clear hierarchies in your application structure.

Important Notes

  • Chain uses Go 1.22's standard pattern matching rules and precedence, so more specific patterns take precedence over more general ones.
  • Path parameters are accessed via Go 1.22's standard r.PathValue("paramName") method.
  • Middleware is applied in the order it's registered, with innermost middleware executed first.
  • Middleware should be registered before the routes it needs to affect.
  • Route groups create isolated middleware stacks that include parent middleware.
  • The response wrapper is automatically enabled when using WithNotFound() or WithMethodNotAllowed().
  • You can explicitly disable the response wrapper with WithoutResponseWrapper() if needed.

License

MIT License

Learn More

For more information about Go 1.22's routing enhancements that Chain builds upon, see the official Go blog post.

Acknowledgments

Chain draws inspiration from alexedwards/flow, a minimal HTTP router for Go. While Flow uses its own pattern matching syntax, Chain adopts Go 1.22's standard library approach for routing patterns, making it a natural choice for projects using Go 1.22+.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Mux

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

Mux is a http.Handler which dispatches requests to different handlers with middleware chaining.

func New

func New() *Mux

New returns a new initialized Mux instance.

func (*Mux) Group

func (m *Mux) Group(fn func(*Mux)) *Mux

Group is used to create 'groups' of routes in a Mux. Middleware registered inside the group will only be used on the routes in that group.

func (*Mux) Handle

func (m *Mux) Handle(pattern string, handler http.Handler) *Mux

Handle registers a new handler for the given request pattern and HTTP method.

func (*Mux) HandleFunc

func (m *Mux) HandleFunc(pattern string, handlerFunc http.HandlerFunc) *Mux

HandleFunc is an adapter which allows using a http.HandlerFunc as a handler.

func (*Mux) ServeHTTP

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP makes the router implement the http.Handler interface.

func (*Mux) Use

func (m *Mux) Use(mw ...func(http.Handler) http.Handler) *Mux

Use registers middleware with the Mux instance. Middleware must have the signature func(http.Handler) http.Handler.

func (*Mux) WithMethodNotAllowed added in v0.2.0

func (m *Mux) WithMethodNotAllowed(handler http.Handler) *Mux

WithMethodNotAllowed sets the method not allowed handler The response wrapper needs to be enabled for this functionality to work

func (*Mux) WithNotFound added in v0.2.0

func (m *Mux) WithNotFound(handler http.Handler) *Mux

WithNotFound sets the not found handler The response wrapper needs to be enabled for this functionality to work

func (*Mux) WithResponseWrapper added in v0.2.0

func (m *Mux) WithResponseWrapper() *Mux

WithResponseWrapper enables the response wrapper

func (*Mux) WithoutResponseWrapper added in v0.2.1

func (m *Mux) WithoutResponseWrapper() *Mux

WithoutResponseWrapper disables the response wrapper

type ResponseWriter added in v0.2.0

type ResponseWriter interface {
	http.ResponseWriter
	Status() int
	Size() int
	Written() bool
}

ResponseWriter extends http.ResponseWriter with additional methods

Jump to

Keyboard shortcuts

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