Dynamic Path Matching with Go and mux
Tags: go, api, backend, webdev
Published: 2023-07-16 13:29:57

Go is by far one of the most interesting languages that I have worked with and I have enjoyed working with it almost everytime (except when functions do not support default values). Recently, I came across this problem where I had a use-case to support user defined routes for an API. Problem was supporting these user defined URL's in real time and actually triggerring a handler based on them.

## More details of the problem

The use-case I had was that the users will be allowed to define any custom route path and some handler logics that should be executed based on that. It's easy to handle the part of triggering the user defined logic based on the route matched. Hard part is to check whether an incoming request is supposed to be matched against an user-defined route or a system route.

<img src="https://raw.githubusercontent.com/deepjyoti30/blog-static-content/master/content/dynamic-url-matching.png" alt="Dynamic URL Matching Problem" />

## Naive Approach that was considered

I considered this naive approach where I would restart the router (using Mux) everytime an user defined route is added/deleted/updated and the router will know whether the incoming request is for an user defined route or system.

This approach is simple enough but is a very bad practice. Imagine thousands of users updating their routes every now and then and the whole router restarts and the API (as a whole) goes down for a few seconds. This can be a major problem for a production API that is customer facing.

Thus, there was need for figuring out an alternate approach to this problem that would be efficient as well.

## How it's Solved

Just a heads up, before I dive into details, the problem was solved by using `mux` and it's useful functions (thanks to the developers for considering a scenario like this while developing).

In order to understand the solution to the problem, we will have to understand how `mux` handles an incoming request. The flow is important here in order to solve this problem in a very efficient way.

In `mux`, every route has two properties (or functions) that can be defined against it. One of them is a `matcher` which can be defined optionally but not necessarily required. The other is a `handler` which is the actual function that will be trigerred if the route is matched.

<img src="https://raw.githubusercontent.com/deepjyoti30/blog-static-content/master/content/mux-route-matching-flow.png" alt="Mux Route Matching Flow" />

### Matchers

`mux` uses matchers to determine whether a route should match. `matchers` are just mere functions which return a boolean value. This value indicates whether the route is matched or not matched.

- `matcher` returns `true`: route is matched and handler should be called.
- `matcher` returns `false`: route is not matched

> NOTE: One thing to know here is that `mux` goes through each route that is defined in the router until a `matcher` returns `true` or the end of routes is reached. When end of routes is reached, a 404 is returned with an error indicating the page is not found.

Matchers are a boon in this particular scenario as this makes solving dynamic user defined routes really simple.

Here's an example matcher that matches the incoming route if it is `/hello`:

```go
func getHelloMatcher() mux.MatcherFunc {
    return func(req *http.Request, rm *mux.RouteMatch) bool {
        return req.URL.Path == "/hello"
    }
}
```

Above is a very simple example of how a custom matcher can be defined but in order to match against a dynamic route, it is better to use `mux`'s own functions. `mux` internally uses a `Match` function that checks if a route matches the incoming requests path and accordingly returns `true` or `false`. It is ideal for us to use this function as they use some complex regex to match the path.

Following code shows how to use `mux`'s internal `Match` function:

```go
func getHelloMatcher() mux.MatcherFunc {
    return func(req *http.Request, rm *mux.RouteMatch) bool {
        // In order to use a internal `mux` function we will need to
        // define a dummy router that can provide the function.
        copyMuxRouter := mux.NewRouter().StrictSlash(true)

        return copyMuxRouter.Methods(http.MethodGet, http.MethodPost).
                Name("dynamic router").
                Path("/hello").
                Match(req, rm)
    }
}
```

As explained, above code uses an internal function to match the incoming route against a route `/hello`. This part of the code can be made dynamic based on user defined route that can be fetched accordigly.

### Handlers

After a `matcher` returns `true`, the route's `handler` is called. In `mux`, all routes will absolutely need a `handler` defined against it or else `mux` will not start the router and throw an error at startup.

Pretty much anyone who has used `mux` is aware of how to define a handler so I will not go over the steps on that. [Here's a full example from mux docs on how to define a handler for a route](https://github.com/gorilla/mux#full-example).

## Enhancements to the above

The problem that I initially mentioned can be solved by using `matchers` as I have explained. However, there are certain places where the code can be made efficient. The `matcher` function that `mux` supports, uses a `RouteMatch` type. This is a custom type inside of which there's a `Vars` key which maps to a `map` type. This `Vars` can be used to pass details from the `matcher` to the `handler` in order to reduce some redundant steps.

As an example, say the user defined route is stored in a database and the user can also define different methods for the route. These details might come handy in the `handler`. In our scenario, these details are definitely required in the `matcher` phase of the code where a database call can be made to get the details for a route.

In the `handler` phase of the flow, instead of making another database call to get the details of the matched route, those details can simply be passed from the `matcher` to the `handler` by using the `Vars` of the `RouteMatch` object.

I am not sure if `RouteMatch.Vars` was intentionally designed to do that but it is definitely a good hack (if you will) to make the code efficient and relatively faster (database network calls are expensive).

Following example shows how some details are passed from the `matcher` to the `handler`:

```go
func getHelloMatcher() mux.MatcherFunc {
    return func(req *http.Request, rm *mux.RouteMatch) bool {
        // In order to use a internal `mux` function we will need to
        // define a dummy router that can provide the function.
        copyMuxRouter := mux.NewRouter().StrictSlash(true)

        methods := [http.MethodGet, http.MethodPost]
        isMatched := copyMuxRouter.Methods(methods...).
                Name("dynamic router").
                Path("/hello").
                Match(req, rm)

        if isMatched {
            rm.Vars["MATCHED_ROUTE_METHODS"] = strings.Join(methods, "-")
        }

        return isMatched
    }
}
```

Following is how to use the `MATCHED_ROUTE_METHODS` in the `handler`:

```go
func getHelloHandler() mux.HandlerFunc {
    return func(rw http.ResponseWriter, req *http.Request) {
        // Read the vars
		vars := mux.Vars(req)

        // Get the methods.
        methods := vars["MATCHED_ROUTE_METHODS"]
        fmt.Println("Matched methods are: ", methods)
    }
}
```

GoLang is becoming one of my favorite languages to work on and I am very close to even replacing Python with GoLang in my next project. I think everyone should give it a try to find out the beauty of it.

`gorrila/mux` team has been doing a great job building this awesome router. Go give it a star at [gorilla/mux](https://github.com/gorilla/mux).

[This post was originally posted at my blog at https://blog.deepjyoti30.dev](https://blog.deepjyoti30.dev/dynamic-path-go-mux)