Routing

Every request goes through the routing in order to reach its handlers, that is how important the routing is to be ergonomic. Flamego spends great amount of effort on designing and implementing the routing capability and future extensibility to ensure your developer happiness.

In Flamego, a route is an HTTP method paired with a URL-matching pattern, and each route takes a chain of handlers.

Below are the helpers to register routes for each HTTP method:

  1. f.Get("/", ...)
  2. f.Patch("/", ...)
  3. f.Post("/", ...)
  4. f.Put("/", ...)
  5. f.Delete("/", ...)
  6. f.Options("/", ...)
  7. f.Head("/", ...)
  8. f.Connect("/", ...)
  9. f.Trace("/", ...)

If you want to match all HTTP methods for a single route, Any is available for you:

  1. f.Any("/", ...)

When you want to match a selected list of HTTP methods for a single route, Routes is your friend:

  1. f.Routes("/", "GET,POST", ...)

Terminology

  • A URL path segment is the portion between two forward slashes, e.g. /<segment>/, the trailing forward slash may not present.
  • A bind parameter uses curly brackets ({}) as its notation, e.g. {<bind_parameter>}, bind parameters are only available in dynamic routes.

Static routes

The static routes are probably the most common routes you have been seeing and using, routes are defined in literals and only looking for exact matches:

  1. f.Get("/user", ...)
  2. f.Get("/repo", ...)

In the above example, any request that is not a GET request to /user or /repo will result in 404.

::: warning Unlike the router in net/http, where you may use /user/ to match all subpaths under the /user path, it is still a static route in Flamego, and only matches a route IFF the request path is exactly the /user/.

Let’s write an example:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Get("/user/", func() string {
  8. return "You got it!"
  9. })
  10. f.Run()
  11. }

Then run some tests as follows:

  1. $ curl http://localhost:2830/user
  2. 404 page not found
  3. $ curl http://localhost:2830/user/
  4. You got it!
  5. $ curl http://localhost:2830/user/info
  6. 404 page not found

:::

Dynamic routes

The dynamic routes, by its name, they match request paths dynamically. Flamego provides most powerful dynamic routes in the Go ecosystem, at the time of writing, there is simply no feature parity you can find in all other existing Go web frameworks.

The flamego.Context provides a family of Param methods to access values that are captured by bind parameters, including:

  • Params returns all bind parameters.
  • Param returns value of the given bind parameter.
  • ParamInt returns value parsed as int.
  • ParamInt64 returns value parsed as int64.

Placeholders

A placeholder captures anything but a forward slash (/), and you may have one or more placeholders within a URL path segment.

Below are all valid usages of placeholders:

  1. f.Get("/users/{name}", ...)
  2. f.Get("/posts/{year}-{month}-{day}.html", ...)
  3. f.Get("/geo/{state}/{city}", ...)

On line 1, the placeholder named {name} to capture everything in a URL path segment.

On line 2, three placeholders {year}, {month} and {day} are used to capture different portions in a URL path segment.

On line 3, two placeholders are used independently in different URL path segments.

Let’s see some examples:

:::: code-group ::: code-group-item Code

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/flamego/flamego"
  6. )
  7. func main() {
  8. f := flamego.New()
  9. f.Get("/users/{name}", func(c flamego.Context) string {
  10. return fmt.Sprintf("The user is %s", c.Param("name"))
  11. })
  12. f.Get("/posts/{year}-{month}-{day}.html", func(c flamego.Context) string {
  13. return fmt.Sprintf(
  14. "The post date is %d-%d-%d",
  15. c.ParamInt("year"), c.ParamInt("month"), c.ParamInt("day"),
  16. )
  17. })
  18. f.Get("/geo/{state}/{city}", func(c flamego.Context) string {
  19. return fmt.Sprintf(
  20. "Welcome to %s, %s!",
  21. strings.Title(c.Param("city")),
  22. strings.ToUpper(c.Param("state")),
  23. )
  24. })
  25. f.Run()
  26. }

::: ::: code-group-item Test

  1. $ curl http://localhost:2830/users/joe
  2. The user is joe
  3. $ curl http://localhost:2830/posts/2021-11-26.html
  4. The post date is 2021-11-26
  5. $ curl http://localhost:2830/geo/ma/boston
  6. Welcome to Boston, MA!

::: ::::

::: tip Try a test request using curl http://localhost:2830/posts/2021-11-abc.html and see what changes. :::

Regular expressions

A bind parameter can be defined with a custom regular expression to capture characters in a URL path segment, and you may have one or more such bind parameters within a URL path segment. The regular expressions are needed to be surrounded by a pair of forward slashes (/<regexp>/).

Below are all valid usages of bind parameters with regular expressions:

  1. f.Get("/users/{name: /[a-zA-Z0-9]+/}", ...)
  2. f.Get("/posts/{year: /[0-9]{4}/}-{month: /[0-9]{2}/}-{day: /[0-9]{2}/}.html", ...)
  3. f.Get("/geo/{state: /[A-Z]{2}/}/{city}", ...)

On line 1, the placeholder named {name} to capture everything in a URL path segment.

On line 2, three placeholders {year}, {month} and {day} are used to capture different portions in a URL path segment.

On line 3, two placeholders are used independently in different URL path segments.

::: tip Because forward slashes are used to indicate the use of regular expressions, they cannot be captured via regular expressions, and will cause a routing parser error when you are trying to do so:

  1. panic: unable to parse route "/{name: /abc\\//}": 1:15: unexpected token "/" (expected "}")

:::

Let’s see some examples:

:::: code-group ::: code-group-item Code

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/flamego/flamego"
  6. )
  7. func main() {
  8. f := flamego.New()
  9. f.Get("/users/{name: /[a-zA-Z0-9]+/}",
  10. func(c flamego.Context) string {
  11. return fmt.Sprintf("The user is %s", c.Param("name"))
  12. },
  13. )
  14. f.Get("/posts/{year: /[0-9]{4}/}-{month: /[0-9]{2}/}-{day: /[0-9]{2}/}.html",
  15. func(c flamego.Context) string {
  16. return fmt.Sprintf(
  17. "The post date is %d-%d-%d",
  18. c.ParamInt("year"), c.ParamInt("month"), c.ParamInt("day"),
  19. )
  20. },
  21. )
  22. f.Get("/geo/{state: /[A-Z]{2}/}/{city}",
  23. func(c flamego.Context) string {
  24. return fmt.Sprintf(
  25. "Welcome to %s, %s!",
  26. strings.Title(c.Param("city")),
  27. c.Param("state"),
  28. )
  29. },
  30. )
  31. f.Run()
  32. }

::: ::: code-group-item Test

  1. $ curl http://localhost:2830/users/joe
  2. The user is joe
  3. $ curl http://localhost:2830/posts/2021-11-26.html
  4. The post date is 2021-11-26
  5. $ curl http://localhost:2830/geo/MA/boston
  6. Welcome to Boston, MA!

::: ::::

::: tip Try doing following test requests and see what changes:

  1. $ curl http://localhost:2830/users/logan-smith
  2. $ curl http://localhost:2830/posts/2021-11-abc.html
  3. $ curl http://localhost:2830/geo/ma/boston

:::

Globs

A bind parameter can be defined with globs to capture characters across URL path segments (including forward slashes). The only notation for the globs is ** and allows an optional argument capture to define how many URL path segments to capture at most.

Below are all valid usages of bind parameters with globs:

  1. f.Get("/posts/{**: **}", ...)
  2. f.Get("/webhooks/{repo: **}/events", ...)
  3. f.Get("/geo/{**: **, capture: 2}", ...)

On line 1, the glob captures everything under the /posts/ path.

On line 2, the glob captures everything in between a path starts with /webhooks/ and ends with /events.

On line 3, the glob captures at most two URL path segments under the /geo/ path.

Let’s see some examples:

:::: code-group ::: code-group-item Code

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/flamego/flamego"
  6. )
  7. func main() {
  8. f := flamego.New()
  9. f.Get("/posts/{**: **}",
  10. func(c flamego.Context) string {
  11. return fmt.Sprintf("The post is %s", c.Param("**"))
  12. },
  13. )
  14. f.Get("/webhooks/{repo: **}/events",
  15. func(c flamego.Context) string {
  16. return fmt.Sprintf("The event is for %s", c.Param("repo"))
  17. },
  18. )
  19. f.Get("/geo/{**: **, capture: 2}",
  20. func(c flamego.Context) string {
  21. fields := strings.Split(c.Param("**"), "/")
  22. return fmt.Sprintf(
  23. "Welcome to %s, %s!",
  24. strings.Title(fields[1]),
  25. strings.ToUpper(fields[0]),
  26. )
  27. },
  28. )
  29. f.Run()
  30. }

::: ::: code-group-item Test

  1. $ curl http://localhost:2830/posts/2021/11/26.html
  2. The post is 2021-11-26.html
  3. $ curl http://localhost:2830/webhooks/flamego/flamego/events
  4. The event is for flamego/flamego
  5. $ curl http://localhost:2830/geo/ma/boston
  6. Welcome to Boston, MA!

::: ::::

::: tip Try doing following test requests and see what changes:

  1. $ curl http://localhost:2830/webhooks/flamego/flamego
  2. $ curl http://localhost:2830/geo/ma/boston/02125

:::

Combo routes

The Combo method can create combo routes when you have different handlers for different HTTP methods of the same route:

  1. f.Combo("/").Get(...).Post(...)

Group routes

Organizing routes in groups not only help code readability, but also encourages code reuse in terms of shared middleware.

It is as easy as wrapping your routes with the Group method, and there is no limit on how many level of nestings you may have:

  1. f.Group("/user", func() {
  2. f.Get("/info", ...)
  3. f.Group("/settings", func() {
  4. f.Get("", ...)
  5. f.Get("/account_security", ...)
  6. }, middleware3)
  7. }, middleware1, middleware2)

The line 4 in the above example may seem unusual to you, but that is not a mistake! The equivalent version of it is as follows:

  1. f.Get("/user/settings", ...)

how does that work

That’s because the Flamego router uses string concatenation to combine group routes.

Huh, so this also works?

  1. f.Group("/user", func() {
  2. f.Get("/info", ...)
  3. f.Group("/sett", func() {
  4. f.Get("ings", ...)
  5. f.Get("ings/account_security", ...)
  6. }, middleware3)
  7. }, middleware1, middleware2)

Yes!

Optional routes

Optional routes may be used for both static and dynamic routes, and use question mark (?) as the notation:

  1. f.Get("/user/?settings", ...)
  2. f.Get("/users/?{name}", ...)

The above example is essentially a shorthand for the following:

  1. f.Get("/user", ...)
  2. f.Get("/user/settings", ...)
  3. f.Get("/users", ...)
  4. f.Get("/users/{name}", ...)

::: warning The optional routes can only be used for the last URL path segment. :::

Matching priority

When your web application grows large enough, you’ll start to want to make sense of which route gets matched at when. This is where the matching priority comes into play.

The matching priority is based on different URL-matching patterns, the matching scope (the narrower scope has the higher priority), and the order of registration.

Here is the breakdown:

  1. Static routes are always being matched first, e.g. /users/settings.
  2. Dynamic routes with placeholders not capturing everything, e.g. /users/{name}.html
  3. Dynamic routes with single placeholder captures everything, e.g. /users/{name}.
  4. Dynamic routes with globs in the middle, e.g. /users/{**: **}/events.
  5. Dynamic routes with globs in the end, e.g. /users/{**: **}.

Constructing URL paths

The URL path can be constructed using the URLPath method if you give the corresponding route a name, which helps prevent URL paths are getting out of sync spread across your codebase:

  1. f.Get("/user/?settings", ...).Name("UserSettings")
  2. f.Get("/users/{name}", ...).Name("UsersName")
  3. f.Get(..., func(c flamego.Context) {
  4. c.URLPath("UserSettings") // => /user
  5. c.URLPath("UserSettings", "withOptional", "true") // => /user/settings
  6. c.URLPath("UsersName", "name", "joe") // => /users/joe
  7. })

Customizing the NotFound handler

By default, the http.NotFound is invoked for 404 pages, you can customize the behavior using the NotFound method:

  1. f.NotFound(func() string {
  2. return "This is a cool 404 page"
  3. })

Auto-registering HEAD method

By default, only GET requests is accepted when using the Get method to register a route, but it is not uncommon to allow HEAD requests to your web application.

The AutoHead method can automatically register HEAD method with same chain of handlers to the same route whenever a GET method is registered:

  1. f.Get("/without-head", ...)
  2. f.AutoHead(true)
  3. f.Get("/with-head", ...)

Please note that only routes that are registered after call of the AutoHead(true) method will be affected, existing routes remain unchanged.

In the above example, only GET requests are accepted for the /without-head path. Both GET and HEAD requests are accepted for the /with-head path.