Skip to content

Request Routing

In the previous chapter, we briefly touched upon request routing. It is time to explore it in more detail.

Routing is how an HTTP request is dispatched to an appropriate request handler. WebGear supports routing based on the request method and/or the request URL path. Let us look at some examples.

Basic Routing

You may use the template haskell quasiquoter route to dispatch a request to a handler based on the URL path. For e.g., [route| /api/time |] timeHandler will dispatch the request to timeHandler if the request path is /api/time.

You may further restrict the routing, by specifying an HTTP method in addition to the path.

import qualified Network.HTTP.Types as HTTP
...
[route| HTTP.GET /api/time |] timeHandler

The above code routes the request to timeHandler only if both the request method and the path matches.

Note

We haven't implemented timeHandler yet and that is intentional. This chapter focuses on routing and we'll not pay much attention to implementing request handlers at this point. For now, you may assume that handlers are arrows that takes a request as input and produces a response as output - just like the hello world example from the previous chapter.

Capturing Path Variables

In many cases, you may want to treat some parts of the path as a variable to be captured for processing later in the handler. For example, the below code can be used to retrieve users based on their id.

import WebGear.Core
import WebGear.Server
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as Wai

userApp :: Wai.Application
userApp = toApplication $
  [route| HTTP.GET /api/user/userId:Int |] getUser

Here, userId is a path variable label that captures an value of type Int. The captured variable value can be accessed by the getUser handler. We'll see how to do that in the next chapter.

You can use any type for the captured variable, provided it has an instance of FromHttpApiData type class. For example, you can modify the above code to use a custom UserId type:

import Web.HttpApiData (FromHttpApiData)

newtype UserId = UserId Int
  deriving newtype (FromHttpApiData)

userApp :: Wai.Application
userApp = toApplication $
  [route| HTTP.GET /api/user/userId:UserId |] getUser

Multiple Endpoints

A typical application will have multiple API endpoints and WebGear supports this using the ArrowPlus typeclass.

Here is an example. If you want two APIs, one to retrieve a user and another to retrieve a book, both can be combined using the <+> operation to form a single application.

myApp :: Wai.Application
myApp = toApplication $ userAPI <+> bookAPI
  where
    userAPI = [route| HTTP.GET /api/user/userId:Int |] getUser
    bookAPI = [route| HTTP.GET /api/book/bookId:Int |] getBook

WebGear will try to route the request in the specified order - first to the user API and then the book API. The request will be dispatched to the route that matches first. If none of the routes match, a 404 (not found) response will be returned.

Path-prefix Matching

In the example above, both the APIs had something in common; they use the GET method and have a path prefix of /api. We had to repeat these on both the handlers and that's not ideal. Fortunately, we do not need to do that.

There is another quasiquoter - match - that captures a prefix of the path. Let us use it to capture the common parts of the APIs and then the route quasiquoter to capture the rest as shown below.

myApp :: Wai.Application
myApp = toApplication $
  [match| HTTP.GET /api |] $ userAPI <+> bookAPI
  where
    userAPI = [route| /user/userId:Int |] getUser
    bookAPI = [route| /book/bookId:Int |] getBook

Eliminate the Quasiquoter

While the quasiquoters are very convenient, not everyone likes them. They feel somewhat magicical and require a language extension. The good news is that you do not have to use them if you don't want to. The route and match quasiquoters are implemented using four functions described below. You may use them instead of route and match.

The first function is method. It inspects the HTTP method of the request and dispatches the request to a specified handler in case of a match.

method HTTP.POST postHandler

This invokes postHandler only if the request method is POST.

The second function is path. It attempts to match a prefix of the request path and dispatches the request to a handler in case of a match.

path "/api" apiHandler

This invokes apiHandler when the request path begins with /api.

The third function is pathVar. Use it to capture a segment of the request path as a path variable.

pathVar @"userId" @Int userHandler

This attempts to parse the next segment of the path as an Int and assign the value to a path variable labelled userId. If that succeeds, userHandler will be invoked.

Finally, we have pathEnd. This function validates that there are no more segments in the request path and invokes a handler in that case.

pathEnd nextHandler

You can chain these functions in any order you want to route the requests. For example:

method HTTP.GET $ path "/api/user" $ pathVar @"userId" @Int $ pathEnd userHandler

This invokes userHandler when the request method is GET, the path is of the format /api/user/<userId>, and the <userId> portion of the path can be parsed as an Int.

The myApp API from the previous example can be rewritten as:

myApp :: Wai.Application
myApp = toApplication $
  method HTTP.GET $ path "/api" $ userAPI <+> bookAPI
  where
    userAPI = path "/user" $ pathVar @"userId" @Int $ pathEnd getUser
    bookAPI = path "/book" $ pathVar @"bookId" @Int $ pathEnd getBook

This version does not use any quasiquoters, but is more verbose. In fact, the quasiquoter generates the above code. It is up to you to choose the version you prefer.

Summary

In this chapter, you learned how to route requests to handlers based on the request method and path. You also learned to implement routing with template haskell quasiquoters and regular functions.

Out-of-the-box, WebGear only supports routing based on request method and path. However, you can easily implement routing based on any other criteria. This is covered in the reference guide in detail.