Skip to content

Routing

The applications we built so far had a single handler. However, most web applications will have many handlers each one for serving different HTTP methods, URL paths etc. Let us see how to achieve this in WebGear.

Alternatives

We know that handlers are kleisli arrows. This means that we can combine multiple handlers using the <|> operator from Alternative type class. Thus, if we have two handlers getTime and setTime, we could create a combined handler like this.

timeHandler = getTime <|> setTime

So, how do we decide which handler gets invoked for a given request? How do we "route" a request to a specific handler? It turns out we can do this using a middleware.

Router Monad

As explained in the traits section, handlers are kleisli arrows on the Router monad. However, it is useful to look at the MonadRouter type class instead of using this monad directly.

class (Alternative m, MonadPlus m) => MonadRouter m where
  -- | Mark the current route as rejected, alternatives can be tried
  rejectRoute :: m a

  -- | Short-circuit the current handler and return a response
  errorResponse :: Response ByteString -> m a

  -- | Handle an error response
  catchErrorResponse :: m a -> (Response ByteString -> m a) -> m a

The rejectRoute method can be used by middlewares and handlers to flag the current route as "not matching", so that the next route is tried. This forms the basis of routing. For example, given this code:

-- Reject the route so that alternatives will be tried
method :: MonadRouter m => Middleware' m req (Method t:req) a a
method handler = Kleisli $ \request -> do
  res <- probe @(Method t) request
  either (const rejectRoute) (runKleisli handler) res

timeHandler = getTime <|> setTime

getTime :: Handler req a
getTime = method @GET getTimeHandler

setTime :: Handler req a
setTime = method @POST setTimeHandler

The MonadRouter will try each route sequentially. If any of them calls rejectRoute, the next one will be tried and the response from the first matching handler will be returned. If none of the route handlers matched, a 404 Not Found response will be returned.

The errorResponse and catchErrorResponse are used in cases where you want to indicate that the current handler matches the route but you want to return an exceptional response. This is similar to the exception handling mechanisms offered by the MonadError type class.

Middlewares for Routing

As you can see from the above description, the routing mechanism is very flexible in WebGear. Virtually any middleware or handler can invoke rejectRoute to skip the current handler and try the next. WebGear does not assume anything about which request attributes are used in the handler selection. That decision is left to middlewares and handlers.

However, for most use cases, you want to route based on the HTTP method and/or the URL path. WebGear provides a number of middlewares that support this common use case.

  • method attempts to match an HTTP method.
  • path attempts to match a prefix portion of the request URL path.
  • pathVar attempts to parse the next component from the URL path to a value via FromHttpApiData type class.
  • pathEnd succeeds only if all URL path components are already consumed by path or pathVar

Here is how you would use them:

-- Matches a GET request on URL /v1/widgets/<widgetId>
-- where <widgetId> can be parsed as an Int
getWidget :: Handler req a
getWidget = method @GET
            $ path @"/v1/widgets"
            $ pathVar @"widgetId" @Int
            $ pathEnd
            $ getWidgetHandler

If you prefer a less verbose version, you can use template haskell quasiquoter:

getWidget :: Handler req a
getWidget = [route| GET /v1/widgets/widgetId:Int |] getWidgetHandler

This version using the route quasiquoter is equivalent to the previous one.

There is also the match quasiquoter which is similar to route but does not add the pathEnd middleware. It is useful in cases where only a prefix of the path needs to be matched.