MCPcopy Index your code
hub / github.com/DATA-DOG/fastroute

github.com/DATA-DOG/fastroute @v1.0.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.0.0 ↗ · + Follow
46 symbols 118 edges 2 files 13 documented · 28%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Build Status GoDoc codecov.io

FastRoute

Insanely simple, idiomatic and fast - 161 loc http router for golang. Uses standard http.Handler and has no limitations to path matching compared to routers derived from Trie (radix) tree based solutions.

Less is exponentially more

fastroute.Router interface extends http.Handler with one extra method - Route in order to route http.Request to http.Handler allowing to chain routes until one is matched.

Go is about composition

The gravest problem all routers have - is the central structure holding all the context.

fastroute is extremely flexible, because it has only static, unbounded functions. Allows unlimited ways to compose router. The exported API is done and will never change, backward compatibility is now guaranteed.

See the following example:

package main

import (
    "fmt"
    "net/http"

    fr "github.com/DATA-DOG/fastroute"
)

var routes = map[string]fr.Router{
    "GET": fr.Chain(
        fr.New("/", handler),
        fr.New("/hello/:name/:surname", handler),
        fr.New("/hello/:name", handler),
    ),
    "POST": fr.Chain(
        fr.New("/users", handler),
        fr.New("/users/:id", handler),
    ),
}

var router = fr.RouterFunc(func(req *http.Request) http.Handler {
    return routes[req.Method] // fastroute.Router is also http.Handler
})

func main() {
    http.ListenAndServe(":8080", router)
}

func handler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, fmt.Sprintf(
        `%s "%s", pattern: "%s", parameters: "%v"`,
        req.Method,
        req.URL.Path,
        fr.Pattern(req),
        fr.Parameters(req),
    ))
}

In overall, it is not all in one router, it is the same http.Handler with do it yourself style, but with zero allocations path pattern matching. Feel free to just copy it and adapt to your needs.

It deserves a quote from Rob Pike:

Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don't get fancy.

The trade off this router makes is the size of n. Instead it provides orthogonal building blocks, just like http.Handler does, in order to build customized routers.

See benchmark results for more details.

Guides

Here are some common usage guidelines:

Custom Not Found handler

Since fastroute.Router returns nil if request is not matched, we can easily extend it and create middleware for it at as many levels as we like.

package main

import (
    "fmt"
    "net/http"

    "github.com/DATA-DOG/fastroute"
)

func main() {
    notFoundHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        w.WriteHeader(404)
        fmt.Fprintln(w, "Ooops, looks like you mistyped the URL:", req.URL.Path)
    })

    router := fastroute.New("/users/:id", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, "user:", fastroute.Parameters(req).ByName("id"))
    })

    http.ListenAndServe(":8080", fastroute.RouterFunc(func(req *http.Request) http.Handler {
        if h := router.Route(req); h != nil {
            return h
        }
        return notFoundHandler
    }))
}

This way, it is possible to extend fastroute.Router with various middleware, including: - Method not found handler. - Fixed path or trailing slash redirects. Based on your chosen route layout. - Options or CORS.

Method not found support

Fastroute provides way to check whether request can be served, not only serve it. Though, the parameters then must be recycled in order to prevent leaking. When a routed request is served, it automatically recycles.

package main

import (
    "fmt"
    "net/http"
    "strings"

    fr "github.com/DATA-DOG/fastroute"
)

var routes = map[string]fr.Router{
    "GET":    fr.New("/users", handler),
    "POST":   fr.New("/users/:id", handler),
    "PUT":    fr.New("/users/:id", handler),
    "DELETE": fr.New("/users/:id", handler),
}

var router = fr.RouterFunc(func(req *http.Request) http.Handler {
    return routes[req.Method] // fastroute.Router is also http.Handler
})

var app = fr.RouterFunc(func(req *http.Request) http.Handler {
    if h := router.Route(req); h != nil {
        return h // routed and can be served
    }

    var allows []string
    for method, routes := range routes {
        if h := routes.Route(req); h != nil {
            allows = append(allows, method)
            fr.Recycle(req) // we will not serve it, need to recycle
        }
    }

    if len(allows) == 0 {
        return nil
    }

    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        w.Header().Set("Allow", strings.Join(allows, ","))
        w.WriteHeader(http.StatusMethodNotAllowed)
        fmt.Fprintln(w, http.StatusText(http.StatusMethodNotAllowed))
    })
})

func main() {
    http.ListenAndServe(":8080", app)
}

func handler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, fmt.Sprintf(
        `%s "%s", pattern: "%s", parameters: "%v"`,
        req.Method,
        req.URL.Path,
        fr.Pattern(req),
        fr.Parameters(req),
    ))
}

If we make a request: curl -i http://localhost:8080/users/1, we will get:

HTTP/1.1 405 Method Not Allowed
Allow: PUT,DELETE,POST
Date: Fri, 19 May 2017 06:09:56 GMT
Content-Length: 19
Content-Type: text/plain; charset=utf-8

Method Not Allowed

Options

Middleware example for OPTIONS:

package main

import (
    "fmt"
    "net/http"
    "strings"

    fr "github.com/DATA-DOG/fastroute"
)

var routes = map[string]fr.Router{
    "GET":    fr.New("/users", handler),
    "POST":   fr.New("/users/:id", handler),
    "PUT":    fr.New("/users/:id", handler),
    "DELETE": fr.New("/users/:id", handler),
}

var router = fr.RouterFunc(func(req *http.Request) http.Handler {
    return routes[req.Method] // fastroute.Router is also http.Handler
})

func main() {
    http.ListenAndServe(":8080", fr.Chain(
        router,          // maybe one of routes
        options(routes), // fallback to options if requested
        // maybe method not allowed
        // maybe redirect fixed path
        // not found then
    ))
}

func handler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, fmt.Sprintf(
        `%s "%s", pattern: "%s", parameters: "%v"`,
        req.Method,
        req.URL.Path,
        fr.Pattern(req),
        fr.Parameters(req),
    ))
}

func options(routes map[string]fr.Router) fr.Router {
    return fr.RouterFunc(func(req *http.Request) http.Handler {
        if req.Method != "OPTIONS" {
            return nil
        }

        fmt.Println(req.URL.Path)
        var allows []string
        for method, routes := range routes {
            if req.URL.Path == "*" {
                // though most of the tools like curl, does not support such a request
                allows = append(allows, method)
                continue
            }

            if h := routes.Route(req); h != nil {
                allows = append(allows, method)
                fr.Recycle(req) // we will not serve it, need to recycle
            }
        }

        if len(allows) == 0 {
            return nil
        }

        allows = append(allows, "OPTIONS")

        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            w.Header().Set("Allow", strings.Join(allows, ","))
        })
    })
}

If we make a request: curl -i -X OPTIONS http://localhost:8080/users/1, we will get:

HTTP/1.1 200 OK
Allow: POST,PUT,DELETE,OPTIONS
Date: Tue, 23 May 2017 07:31:47 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

Combining static routes

The best and fastest way to match static routes - is to have a map of path -> handler pairs.

package main

import (
    "fmt"
    "net/http"

    "github.com/DATA-DOG/fastroute"
)

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, req.URL.Path, fastroute.Parameters(req))
    })

    static := map[string]http.Handler{
        "/status":      handler,
        "/users/roles": handler,
    }

    staticRoutes := fastroute.RouterFunc(func(req *http.Request) http.Handler {
        return static[req.URL.Path]
    })

    dynamicRoutes := fastroute.Chain(
        fastroute.New("/users/:id", handler),
        fastroute.New("/users/:id/roles", handler),
    )

    http.ListenAndServe(":8080", fastroute.Chain(staticRoutes, dynamicRoutes))
}

Trailing slash or fixed path redirects

In cases when your API faces public, it might be a good idea to redirect with corrected request URL if user makes a simple mistake.

This fixes trailing slash, case mismatch and cleaned path all at once. Note, we should follow some specific rule, how we build our path patterns in order to be able to fix them. In this case we follow all lowercase rule for static segments, parameters may match any case.

package main

import (
    "fmt"
    "net/http"
    "path"
    "strings"

    "github.com/DATA-DOG/fastroute"
)

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, req.URL.Path, fastroute.Parameters(req))
    })

    // we follow the lowercase rule for static segments
    router := fastroute.Chain(
        fastroute.New("/status", handler),
        fastroute.New("/users/:id", handler),
        fastroute.New("/users/:id/roles/", handler), // one with trailing slash
    )

    http.ListenAndServe(":8080", redirectTrailingOrFixedPath(router))

    // requesting: http://localhost:8080/Users/5/Roles
    // redirects: http://localhost:8080/users/5/roles/
}

func redirectTrailingOrFixedPath(router fastroute.Router) fastroute.Router {
    return fastroute.RouterFunc(func(req *http.Request) http.Handler {
        if h := router.Route(req); h != nil {
            return h // has matched, no need for fixing
        }

        p := strings.ToLower(path.Clean(req.URL.Path)) // first clean path and lowercase
        attempts := []string{p}                        // first variant with cleaned path
        if p[len(p)-1] == '/' {
            attempts = append(attempts, p[:len(p)-1]) // without trailing slash
        } else {
            attempts = append(attempts, p+"/") // with trailing slash
        }

        try, _ := http.NewRequest(req.Method, "/", nil) // make request for all attempts
        for _, attempt := range attempts {
            try.URL.Path = attempt
            if h := router.Route(try); h != nil {
                // matched, resolve fixed path and redirect
                pat, params := fastroute.Pattern(try), fastroute.Parameters(try)
                var fixed []string
                var nextParam int
                for _, segment := range strings.Split(pat, "/") {
                    if strings.IndexAny(segment, ":*") != -1 {
                        fixed = append(fixed, params[nextParam].Value)
                        nextParam++
                    } else {
                        fixed = append(fixed, segment)
                    }
                }
                fastroute.Recycle(try)
                return redirect(strings.Join(fixed, "/"))
            }
        }
        return nil // could not fix path
    })
}

func redirect(fixedPath string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        req.URL.Path = fixedPath
        http.Redirect(w, req, req.URL.String(), http.StatusPermanentRedirect)
    })
}

Named routes

This is trivial to implement a package inside your project, where all your routes used may be named. And later paths built by these named routes from anywhere within your application.

package routes

import (
    "fmt"
    "strings"

    "github.com/DATA-DOG/fastroute"
)

var all = make(map[string]string)

func Named(name, path string, handler interface{}) fastroute.Router {
    if p, dup := all[name]; dup {
        panic(fmt.Sprintf(`route: "%s" at path: "%s" was already registered for path: "%s"`, name, path, p))
    }
    all[name] = path
    return fastroute.New(path, handler)
}

func Get(name string, params fastroute.Params) string {
    p, ok := all[name]
    if !ok {
        panic(fmt.Sprintf(`route: "%s" was never registered`, name))
    }
    for _, param := range params {
        if key := ":" + param.Key; strings.Index(p, key) != -1 {
            p = strings.Replace(p, key, param.Value, 1)
        } else if key = "*" + param.Key; strings.Index(p, key) != -1 {
            p = strings.Replace(p, key, param.Value, 1)
        }
    }

    if strings.IndexAny(p, ":*") != -1 {
        panic(fmt.Sprintf(`not all parameters were set: "%s" for route: "%s"`, p, name))
    }
    return p
}

Then the usage is obvious:

package main

import (
    "fmt"
    "net/http"

    "github.com/DATA-DOG/fastroute"
    "github.com/DATA-DOG/fastroute/routes" // should be somewhere in your project
)

func main() {
    router := fastroute.Chain(
        routes.Named("home", "/", handler),
        routes.Named("hello-full", "/hello/:name/:surname", handler),
    )

    fmt.Println(routes.Get("hello-full", fastroute.Params{
        {"name", "John"},
        {"surname", "Doe"},
    }))

    http.ListenAndServe(":8080", router)
}

func handler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, fmt.Sprintf(`%s "%s"`, req.Method, req.URL.Path))
}

Hit counting frequently accessed routes

In cases where n number of routes is very high and it is unknown what routes would be most frequently accessed or it changes during runtime, in order to highly improve performance, you can use hit count based reordering middleware.

``` go package main

import ( "fmt" "net/http" "sort" "sync"

fr "github.com/DATA-DOG/fastroute"

)

var routes = map[string]fr.Router{ "GET": fr.Chain( // here follows frequently accessed routes HitCountingOrderedChain( fr.New("/", handler), fr.New("/health", handler), fr.New("/status", handler), ), // less frequently accessed routes fr.New("/hello/:name/:surname", handler), fr.New("/hello/:name", handler), ), "POST": fr.Chain( fr.New("/users", handler), fr.New("/users/:id", handler), ), }

// serves routes by request method var router = fr.RouterFunc

Extension points exported contracts — how you extend this code

Router (Interface)
Router interface extends http.Handler with one extra method - Route in order to route http.Request to http.Handler allow
router.go
RouterFunc (FuncType)
RouterFunc type is an adapter to allow the use of ordinary functions as Routers. If f is a function with the appropriate
router.go

Core symbols most depended-on inside this repo

ByName
called by 12
router.go
ServeHTTP
called by 10
router.go
Route
called by 9
router.go
push
called by 2
router.go
reset
called by 2
router.go
match
called by 1
router.go
Parameters
called by 0
router.go
Pattern
called by 0
router.go

Shape

Function 30
Method 9
TypeAlias 3
Struct 2
FuncType 1
Interface 1

Languages

Go100%

Modules by API surface

router_test.go30 symbols
router.go16 symbols

For agents

$ claude mcp add fastroute \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page