Servers for Routing Types

The primary use of routing types in Hyper is for writing web servers. The purescript-hyper-routing-server package provides a router middleware which, together with our handler and rendering functions, gives us a full-fledged server.

A Single-Resource Example

Let’s say we want to render a home page as HTML. We start out by declaring the data type Home, and the structure of our site:

data Home = Home

type Site1 = Resource (Get Home) HTML

Resource (Get Home) HTML is a routing type with only one resource, responding to HTTP GET requests, rendering a Home value as HTML. So where does the Home value come from? We provide it using a handler. A handler for Site1 would be some value of the following type:

forall m. Monad m => ExceptT RoutingError m Home

We can construct such a value using pure and a Home value:

home :: forall m. Applicative m => m Home
home = pure Home

Nice! But what comes out on the other end? We need something that renders the Home value as HTML. By providing an instance of EncodeHTML for Home, we instruct the resource how to render.

instance encodeHTMLHome :: EncodeHTML Home where
  encodeHTML Home =
    p (text "Welcome to my site!")

The HTML type is a phantom type, only used as a marker type, and the actual markup is written in the MarkupM DSL from purescript-smolder.

We are getting ready to create the server. First, we need a value-level representation of the Site1 type, to be able to pass it to the router function. For that we use Proxy. Its documentation describes it as follows:

The Proxy type and values are for situations where type information is required for an input to determine the type of an output, but where it is not possible or convenient to provide a value for the input.

We create a top-level definition of the type Proxy Site1 with the value constructor Proxy.

site1 :: Proxy Site1
site1 = Proxy

We pass the proxy, our handler, and the onRoutingError function for cases where no route matched the request, to the router function.

onRoutingError status msg =
  writeStatus status
  :*> contentType textHTML
  :*> closeHeaders
  :*> respond (maybe "" id msg)

siteRouter = router site1 home onRoutingError

The value returned by router is regular middleware, ready to be passed to a server.

main :: forall e. Eff (http :: HTTP, console :: CONSOLE, buffer :: BUFFER | e) Unit
main =
  runServer defaultOptions {} siteRouter

Routing Multiple Resources

Real-world servers often need more than one resource. Let’s define a router for an application that shows a home page with links, a page listing users, and a page rendering a specific user.

data Home = Home

data AllUsers = AllUsers (Array User)

newtype User = User { id :: Int, name :: String }

type Site2 =
  Resource (Get Home) HTML
  :<|> "users" :/ Resource (Get AllUsers) HTML
  :<|> "users" :/ Capture "user-id" Int :> Resource (Get User) HTML

site2 :: Proxy Site2
site2 = Proxy

Let’s go through the new constructs used:

  • :<|> is a type operator that separates alternatives. A router for this type will try each route in order until one matches.
  • :/ separates a literal path segment and the rest of the routing type.
  • Capture takes a descriptive string and a type. It takes the next available path segment and tries to convert it to the given type. Each capture in a routing type corresponds to an argument in the handler function.
  • :> separates a routing type modifier, like Capture, and the rest of the routing type.

We define handlers for our resource methods as regular functions on the specified data types, returning ExceptT RoutingError m a values, where m is the monad of our middleware, and a is the type to render for the resource.

home :: forall m. Monad m => ExceptT RoutingError m Home
home = pure Home

allUsers :: forall m. Monad m => ExceptT RoutingError m AllUsers
allUsers = AllUsers <$> getUsers

getUser :: forall m. Monad m => Int -> ExceptT RoutingError m User
getUser id' =
  find userWithId <$> getUsers >>=
  case _ of
    Just user -> pure user
    Nothing ->
      throwError (HTTPError { status: statusNotFound
                            , message: Just "User not found."
    userWithId (User u) = == id'

As in the single-resource example, we want to render as HTML. Let’s create instances for our data types. Notice how we can create links between routes in a type-safe manner.

instance encodeHTMLHome :: EncodeHTML Home where
  encodeHTML Home =
    case linksTo site2 of
      _ :<|> allUsers' :<|> _ ->
        p do
          text "Welcome to my site! Go check out my "
          linkTo allUsers' (text "Users")
          text "."

instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
  encodeHTML (AllUsers users) =
    div do
      h1 (text "Users")
      ul (traverse_ linkToUser users)
      linkToUser (User u) =
        case linksTo site2 of
          _ :<|> _ :<|> getUser' ->
            li (linkTo (getUser' (text

instance encodeHTMLUser :: EncodeHTML User where
  encodeHTML (User { name }) =
    h1 (text name)

The pattern match on the value returned by linksTo must match the structure of the routing type. We use :<|> to pattern match on links. Each matched link will have a type based on the corresponding resource. getUser in the previous code has type Int -> URI, while allUsers has no captures and thus has type URI.

We are still missing getUsers, our source of User values. In a real application it would probably be a database query, but for this example we simply hard-code some famous users of proper instruments.

getUsers :: forall m. Applicative m => m (Array User)
getUsers =
  [ User { id: 1, name: "John Paul Jones" }
  , User { id: 2, name: "Tal Wilkenfeld" }
  , User { id: 3, name: "John Patitucci" }
  , User { id: 4, name: "Jaco Pastorious" }

Almost done! We just need to create the router, and start a server.

main :: forall e. Eff (http :: HTTP, console :: CONSOLE, buffer :: BUFFER | e) Unit
main =
  let otherSiteRouter =
        router site2 (home :<|> allUsers :<|> getUser) onRoutingError

      onRoutingError status msg =
        writeStatus status
        :*> contentType textHTML
        :*> closeHeaders
        :*> respond (maybe "" id msg)

  in runServer defaultOptions {} otherSiteRouter

Notice how the composition of handler functions, using the value-level operator :<|>, matches the structure of our routing type. If we fail to match the type we get a compile error.

Multi-Method Resources

So far we have just used a single method per resource, the Get method. By replacing the single method type with a sequence of alternatives, constructed with the type-level operator :<|>, we get a resource with multiple methods.

type MultiMethodExample =
  Resource (Get User :<|> Delete User) HTML

MultiMethodExample is a routing type with a single resource, which has multiple resource methods. Handlers for the resource methods needs to be separated by the value-level operator :<|>, just as with handlers for different resources.

router site (getUsers :<|> deleteUser) onRoutingError

Content Negotiation

By specifying alternative content types for a resource, Hyper can choose a response and content type based on the request Accept header. This is called content negotiation. Instead of specifying a single type, like HTML or JSON, we provide alternatives using :<|>. All content types must have MimeRender instances for the response body type.

type Site3 =
  Resource (Get Home) HTML
  :<|> "users" :/ Resource (Get AllUsers) (HTML :<|> JSON)
  :<|> "users" :/ Capture "user-id" Int :> Resource (Get User) (HTML :<|> JSON)

By making requests to this site, using Accept headers, we can see how the router chooses the matching content type (output formatted and shortened for readability).

$ <strong>curl -H 'Accept: application/json' http://localhost:3000/users</strong>
    "name": "John Paul Jones",
    "id": "1"
    "name": "Tal Wilkenfeld",
    "id": "2"

There is support for wildcards and qualities as well.

$ curl -H 'Accept: text/*;q=1.0' http://localhost:3000/users
    <li><a href="/users/1">John Paul Jones</a></li>
    <li><a href="/users/2">Tal Wilkenfeld</a></li>