Advanced URL path template parsing and routing for Go with rich type validation, extensible constraints, and developer-friendly error messages—zero dependencies, production-ready.
Most Go routing libraries require verbose handler registration or lack type validation. pathvars provides:
- ✅ Declarative routing with templates like
GET /users/{id:int}/posts/{slug:slug:length[5..50]} - ✅ Rich type system - 11+ built-in types with extensible validation
- ✅ Powerful constraints - Range, length, regex, enum, format, and more
- ✅ Developer-friendly errors - Clear messages with suggestions for fixes
- ✅ Zero dependencies - Only Go standard library
- ✅ Production-ready - Comprehensive tests, proven in real applications
- ✅ Security-first - Prevents identifier injection by design
go get github.com/mikeschinkel/go-pathvarsRequirements:
- Go 1.25+
- Set environment variable:
export GOEXPERIMENT=jsonv2
Note: The package uses Go's experimental JSON v2 API for enhanced JSON handling.
- Extended URI template syntax:
{name:type:constraint}with implicit type inference - 11+ built-in types: int, string, uuid, slug, date, boolean, decimal, real, alphanumeric, identifier, email
- Extensible constraint system: range, length, enum, regex, format, notempty
- Multi-segment parameters:
{path*:string}captures multiple path segments - Query parameter support:
?{limit?10:int:range[1..100]} - HTTP method matching:
GET /path,POST /path, or just/path(any method) - Detailed validation errors: RFC 9457-compliant error messages
- Memory efficient: Value returns, pre-compiled regex
- Date/time format constraints: Creative formats like
format[the-year-yyyy-month-mm-day-dd] - UUID version validation: v1-v8, ULID, KSUID, NanoID support
- Implicit type inference:
{int}infers int type,{slug::enum[a,b]}infers slug with constraint - Default values:
{limit?20:int}for optional parameters - Fail-fast validation: Configuration errors caught at startup
- Comprehensive test coverage: Unit and integration tests included
package main
import (
"fmt"
"log"
"net/http"
"github.com/mikeschinkel/go-pathvars"
)
func main() {
// Create and configure router
router := pathvars.NewRouter()
// Add routes with typed, validated parameters
// Routes are compiled as they are added - ready to use!
router.AddRoute("GET", "/users/{id:int}", nil)
router.AddRoute("GET", "/posts/{slug:slug:length[5..50]}", nil)
router.AddRoute("GET", "/products?{category:string}&{limit?20:int:range[1..100]}", nil)
// Handle requests
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
result, err := router.Match(r.Method, r.URL.Path)
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
// Access extracted parameters
userID, _ := result.GetValue("id")
fmt.Fprintf(w, "Matched route %d, id=%s\n", result.Index, userID)
})
log.Println("Server running on :8080")
http.ListenAndServe(":8080", nil)
}Try it:
curl http://localhost:8080/users/123 # ✓ Matches, id=123
curl http://localhost:8080/users/abc # ✗ 422 validation error
curl http://localhost:8080/posts/hello # ✗ slug too short
curl http://localhost:8080/posts/hello-world # ✓ Matches- API servers with declarative routing configuration
- Microservices needing type-safe parameter extraction
- REST APIs requiring robust URL validation
- Applications wanting clear error messages for API consumers
- Projects preferring zero-dependency solutions
PathVars employs a comprehensive multi-layered testing strategy:
- Unit Tests - ~90%+ coverage of core parsing, matching, and validation logic
- Fuzz Testing - Go native fuzzing with 84 seed cases and timeout protection
- Corpus Regression - Fast regression testing of all discovered fuzz inputs
Key Results (v0.1.0):
- ✅ 50K-120K fuzzing executions/second
- ✅ 152 interesting inputs discovered in initial 30-second run
- ✅ Zero panics, zero infinite loops found during fuzzing
- ✅ Zero known security vulnerabilities
Running Tests:
# Run all tests
go test -v ./test
# Run fuzzing (local development)
go test -fuzz=FuzzParseTemplate -fuzztime=1m ./test
# Run corpus regression (CI/CD)
go test -v -run=TestFuzzCorpus ./testSee Testing Strategy ADR for complete details on our testing approach.
- Architecture ADR - Design decisions and principles
- Error Handling ADR - Error composition patterns
- Testing Strategy ADR - Multi-layered testing approach
- Arrays & Rows ADR - Future array/row syntax (proposed)
- API Reference - Complete API documentation below
- pkg.go.dev - GoDoc documentation
The main routing engine that compiles and matches routes.
type Router struct {
// Contains private fields
}Functions:
NewRouter() *Router- Creates a new router instance(r *Router) AddRoute(method HTTPMethod, path Template, args *RouteArgs) error- Adds a route to the router (routes are compiled immediately)(r *Router) Match(*http.Request) (pathvars.MatchResult, error)- Matches HTTP request against routes
Type aliases for path specifications and components.
type PathSpec string // e.g., "GET /users/{id}" or "/users/{id}"
type Method string // HTTP method like "GET", "POST"
type Path string // URL path like "/users/{id}"Functions:
ParsePathSpec(spec PathSpec) (method string, path string, err error)- Splits path specification into method and path
Represents a compiled HTTP endpoint with method, template, and routing index.
type Route struct {
Method string // HTTP method (empty = any method)
Template *Template // Parsed path template
Index int // Position in router's route list
}Represents individual parts of a path template (literal strings or parameter placeholders).
type Segment stringMethods:
(s Segment) IsLiteral() bool- Returns true if segment is literal string (not parameter)(s Segment) IsParameter() bool- Returns true if segment is parameter placeholder
Represents a path or query parameter with type and validation rules.
type Parameter struct {
// Contains private fields
}Creation:
NewParameter(args ParameterArgs) Parameter- Creates a new parameter instanceParseParameter(spec string, position int) (Parameter, error)- Parses parameter from specification string
Methods:
(p Parameter) DataType() PVDataType- Returns parameter's data type(p Parameter) Name() string- Returns parameter name(p Parameter) IsOptional() bool- Returns true if parameter is optional(p Parameter) IsMultiSegment() bool- Returns true if parameter spans multiple path segments(p Parameter) DefaultValue() *string- Returns default value if any
Configuration struct:
type ParameterArgs struct {
Name string
UseType ParamUseType
DataType PVDataType
Constraints []Constraint
Position int
Original string
MultiSegment bool
Optional bool
DefaultValue *string
}Indicates how a parameter is used.
type ParamUseType int
const (
UnspecifiedParameterType ParamUseType = iota
PathParameter // Extracted from URL path
QueryParameter // Extracted from query string
)Represents a parsed path template with parameters and compiled regex.
type Template struct {
// Contains private fields
}Creation:
ParseTemplate(template string) (*Template, error)- Parses template string into Template object
Methods:
(t *Template) Match(path, queryString string) (ValuesMap, bool)- Matches path and query against template(t *Template) Parameters() []Parameter- Returns all parameters (TODO: implementation needed)(t *Template) Validate(params map[string]string) error- Validates parameter values (TODO: implementation needed)(t *Template) Substitute(values map[string]string) (string, error)- Builds path from values (TODO: implementation needed)
Contains the results of matching an HTTP request against routes.
type MatchResult struct {
Index int // Which route matched
// Contains private fields
}
type ValuesMap map[string]stringCreation:
NewMatchResult(index int, valuesMap ValuesMap) MatchResult- Creates new match result
Methods:
(m MatchResult) ParamsMap() ValuesMap- Returns extracted parameter values(m MatchResult) GetValue(name string) (value string, found bool)- Gets specific parameter value(m MatchResult) VarCount() int- Returns number of extracted parameters(m MatchResult) HasVars() bool- Returns true if any parameters were extracted(m MatchResult) ForEachVar(fn func(name, value string) bool)- Iterates over parameters
Enumerated data types for parameter validation.
type PVDataType int
const (
UnspecifiedType PVDataType = iota
StringType
IntegerType
RealType
DecimalType
IdentifierType
DateType
UUIDType
AlphanumericType
SlugType
BooleanType
EmailType
)Methods:
(dt PVDataType) TypeName() PVDataTypeName- Returns canonical string name
Functions:
ParsePVDataType(typeStr string) (PVDataType, error)- Converts string to data typeInferDataTypeFromName(name string) (PVDataType, bool)- Infers type from parameter name
String representation of data types.
type PVDataTypeName string
const (
StringTypeName PVDataTypeName = "string"
IntegerTypeName PVDataTypeName = "integer"
IntTypeName PVDataTypeName = "int" // Alias for integer
DecimalTypeName PVDataTypeName = "decimal"
RealTypeName PVDataTypeName = "real"
IdentifierTypeName PVDataTypeName = "identifier"
DateTypeName PVDataTypeName = "date"
UUIDTypeName PVDataTypeName = "uuid"
AlphanumericTypeName PVDataTypeName = "alphanumeric"
AlphanumTypeName PVDataTypeName = "alphanum" // Alias for alphanumeric
SlugTypeName PVDataTypeName = "slug"
BooleanTypeName PVDataTypeName = "boolean"
BoolTypeName PVDataTypeName = "bool" // Alias for boolean
EmailTypeName PVDataTypeName = "email"
)Defines parameter validation constraints.
type Constraint interface {
Validate(value string) error
String() string
Type() ConstraintType
Parse(value string, dataType PVDataType) (Constraint, error)
ValidDateTypes() []PVDataType
MapKey(dt PVDataTypeName) ConstraintMapKey
EnsureBaseConstraint(Constraint)
}Types of validation constraints.
type ConstraintType string
const (
FormatConstraintType ConstraintType = "format"
EnumConstraintType ConstraintType = "enum"
LengthConstraintType ConstraintType = "length"
NotEmptyConstraintType ConstraintType = "notempty"
RangeConstraintType ConstraintType = "range"
RegexConstraintType ConstraintType = "regex"
)Functions:
ParseConstraints(spec string, dataType PVDataType) ([]Constraint, error)- Parses constraint specifications
Functions for managing constraint types and data type aliases.
type ConstraintMapKey string
type ConstraintsMap map[ConstraintMapKey]Constraint
type DataTypeAliasMap = map[PVDataTypeName]PVDataTypeNameFunctions:
RegisterDataTypeAlias(dataType PVDataType, alias PVDataTypeName)- Registers type aliasRegisterConstraint(c Constraint)- Registers a constraint implementationGetConstraintsMap() ConstraintsMap- Returns the global constraints mapGetConstraintMapKey(ct ConstraintType, dtn PVDataTypeName) ConstraintMapKey- Generates constraint keyGetConstraint(ct ConstraintType, dt PVDataType) (Constraint, error)- Retrieves constraint by type
The package provides several built-in constraint implementations:
DateFormatConstraint:
type DateFormatConstraint struct { /* private fields */ }NewDateFormatConstraint(format string, parser func(string) (time.Time, error)) *DateFormatConstraintParseDateFormatConstraint(spec string) (*DateFormatConstraint, error)
DateRangeConstraint:
type DateRangeConstraint struct { /* private fields */ }NewDateRangeConstraint(min time.Time, max time.Time) *DateRangeConstraintParseDateRangeConstraint(rangeSpec string) (*DateRangeConstraint, error)
DecimalRangeConstraint:
type DecimalRangeConstraint struct { /* private fields */ }NewDecimalRangeConstraint(min float64, max float64) *DecimalRangeConstraintParseDecimalRangeConstraint(rangeSpec string) (*DecimalRangeConstraint, error)
EnumConstraint:
type EnumConstraint struct { /* private fields */ }NewEnumConstraint(values map[string]bool, list []string) *EnumConstraintParseEnumConstraint(enumSpec string) (*EnumConstraint, error)
IntegerRangeConstraint:
type IntegerRangeConstraint struct { /* private fields */ }NewIntRangeConstraint(min int64, max int64) *IntegerRangeConstraintParseIntRangeConstraint(rangeSpec string) (*IntegerRangeConstraint, error)
LengthConstraint:
type LengthConstraint struct { /* private fields */ }NewLengthConstraint(min int, max int) *LengthConstraintParseLengthConstraint(rangeSpec string) (*LengthConstraint, error)
NotEmptyConstraint:
type NotEmptyConstraint struct { /* private fields */ }NewNotEmptyConstraint() *NotEmptyConstraintParseNotEmptyConstraint(value string) (*NotEmptyConstraint, error)
RegexConstraint:
type RegexConstraint struct { /* private fields */ }NewRegexConstraint(regex *regexp.Regexp, raw string) *RegexConstraintParseRegexConstraint(pattern string) (*RegexConstraint, error)
UUIDFormatConstraint:
type UUIDFormatConstraint struct { /* private fields */ }NewUUIDFormatConstraint(format string, validator func(string) error) *UUIDFormatConstraintParseUUIDFormatConstraint(spec string) (*UUIDFormatConstraint, error)
Utility Functions:
ParseRangeConstraint(rangeSpec string, dataType PVDataType) (Constraint, error)- Generic range constraint parser
The package defines several sentinel error values for different failure scenarios:
var (
ErrInvalidTemplate = errors.New("invalid template syntax")
ErrUnmatchedBrace = errors.New("unmatched brace in template")
ErrInvalidParameter = errors.New("invalid parameter")
ErrInvalidType = errors.New("unknown parameter type")
ErrInvalidConstraint = errors.New("invalid constraint syntax")
ErrNoMatch = errors.New("no matching route")
ErrAPIRouterNotCompiled = errors.New("API router not compiled; must be compiled before calling Match()")
ErrValidationFailed = errors.New("parameter validation failed")
ErrUnknownConstraintType = errors.New("unknown constraint type")
ErrInvalidSyntax = errors.New("invalid syntax")
ErrParseFailed = errors.New("parse failed")
)All errors provide detailed context including the failing value, expected format, and error location through error wrapping.
Parameters use a flexible syntax in path templates:
{name}- String parameter, type inferred from name if possible{name:type}- Explicit data type{name:type:constraints}- Type with validation constraints{name::constraints}- Inferred type with constraints (double colon)
{name?}- Optional parameter, no default{name?default}- Optional parameter with default value
{name*}- Captures multiple path segments{name*?}- Optional multi-segment parameter
{id:int:range[1..1000]}- Integer between 1 and 1000{email:string:regex[.+@.+]}- String matching email pattern (auto-anchored for full match){status:string:enum[active,inactive]}- String from allowed values{name:string:length[3..50]}- String with length constraints{slug:string:notempty}- Non-empty string{date:date:format[yyyy-mm-dd]}- Date with specific format
{id:string:regex[[0-9]+],length[3..10]}- Multiple constraints separated by commas
Note on Regex Constraints: Regex patterns automatically match the complete parameter value (full string matching). Do not include ^ (start) or $ (end) anchors in your patterns - they are added automatically to ensure security and prevent partial matches. For example, regex[.+@.+] internally becomes ^.+@.+$ before compilation.
router.AddRoute("GET", "/users/{id:int}", nil)router.AddRoute("GET", "/users/{id:int:range[1..1000]}", nil)params := []Parameter{
NewParameter(ParameterArgs{
Name: "limit",
UseType: QueryParameter,
DataType: IntegerType,
Optional: true,
DefaultValue: stringPtr("10"),
}),
}
router.AddRoute("GET", "/users", &RouteArgs{Parameters: params})router.AddRoute("GET", "/posts/{category?general:string}", nil)router.AddRoute("GET", "/files/{path*:string}", nil)router.AddRoute("GET", "/api/users/{id:uuid}", &RouteArgs{
Index: 0, // Explicit index (optional - auto-defaults if 0)
Description: "Retrieve user by UUID",
Cardinality: CardinalityOne, // Expect single row result
RowType: DBRowTypeJSON, // Return as JSON object
ColumnTypes: []DBDataType{ // Expected column types
DBDataTypeUUID,
DBDataTypeString,
DBDataTypeString,
},
})This README provides comprehensive documentation of all public APIs in the pathvars package, including types, functions, methods, constants, and usage examples.
Contributions are welcome! This project is open to external contributors.
Before contributing:
- Read the Architecture ADR to understand design principles
- Check existing issues and pull requests
- Open an issue to discuss significant changes before implementing
Development:
# Clone the repository
git clone https://github.com/mikeschinkel/go-pathvars.git
cd go-pathvars
# Run tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Build
go build ./...Code style:
- Follow standard Go conventions (
gofmt,go vet) - Write tests for new features
- Update documentation for API changes
- Keep commits focused and atomic
MIT License - see LICENSE file for details.
This package was extracted from the xmlui-test-server project, where it proved its value in production use. The extraction makes it available as a standalone, reusable component for the Go community.
Related projects:
- go-sqlparams - SQL parameter placeholder conversion