Hypertrout

Build servers in Hyper using Trout

Contents

Purpose

The purpose of this package, Hypertrout, is writing web servers using the type-level routing API in Trout. It provides a router middleware which, together with records of handler functions for resources, and rendering instances, 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 inside a resource record. A resource record for Site1 would be some value of the following type:

forall m. Monad m => {"GET" :: ExceptT RoutingError m Home}

The resource record has fields for each supported HTTP method, with values being the corresponding handlers. A resource record type, supporting both GET and POST, could have the following type:

forall m. Monad m => { "GET" :: ExceptT RoutingError m SomeType
                     , "POST" :: ExceptT RoutingError m SomeType
                     }

We can construct a resource record for the Site1 routing type using pure and a Home value:

home :: forall m. Applicative m => {"GET" :: m Home}
home = {"GET": 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. To combine multiple resources, resource routing types are separated using the :<|> operator, the type-level operator for separating alternatives.

RoutingType1 :<|> RoutingType2 :<|> ... :<|> RoutingTypeN

When combining multiple resources in a routing type, each resource has to be named. The := type-level operator names a resource, or another nested structure of resources, using a Symbol on the left-hand side, and a routing type on the right-hand side.

"<resource-name>" := RoutingType

The following is a routing type for two resources, named "foo" and "bar":

     "foo" := Resource (Get Foo HTML)
:<|> "bar" := Resource (Get Bar HTML)

Named routes can be nested to create a structure of arbitrary depth, a practice useful for grouping related resources:

type UserResources =
       "profile"  := Resource (Get UserProfile HTML)
  :<|> "settings" := Resource (Get UserSettings HTML)

type AdminResources =
       "users" := Resource (Get Users HTML)
  :<|> "logs"  := Resource (Get Logs HTML)

type MyNestedResources =
       "user"  := UserResources
  :<|> "admin" := AdminResources

Example

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 =
       "home"  := Resource (Get Home HTML)
  :<|> "users" := "users" :/ Resource (Get AllUsers HTML)
  :<|> "user"  := "users" :/ Capture "user-id" Int
                          :> Resource (Get User HTML)

site2 :: Proxy Site2
site2 = Proxy

There are some new things in this code that we haven’t talked about, and some we touched upon a bit. Here’s a walk-through of what’s going on:

We define a resource record using 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 and method.

homeResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m Home}
homeResource = {"GET": pure Home}

usersResource :: forall m. Monad m => {"GET" :: ExceptT RoutingError m AllUsers}
usersResource = {"GET": AllUsers <$> getUsers}

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

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 =
    let {users} = linksTo site2
    in p do
      text "Welcome to my site! Go check out my "
      linkTo users (text "Users")
      text "."

instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
  encodeHTML (AllUsers users) =
    div do
      h1 (text "Users")
      ul (traverse_ linkToUser users)
    where
      linkToUser (User u) =
        let {user} = linksTo site2
        in li (linkTo (user u.id) (text u.name))

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

The record destructuring on the value returned by linksTo extracts the correct link, based on the names from the routing type. Each link will have a type based on the corresponding resource. user in the previous code has type Int -> URI, while users 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 =
  pure
  [ 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 resources = { home: homeResource
                  , users: usersResource
                  , user: userResource
                  }

      otherSiteRouter =
        router site2 resources onRoutingError

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

  in runServer defaultOptions {} otherSiteRouter

Notice how the resources record matches the names and 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 =
  "user" := Resource (Get User HTML :<|> Delete User HTML)

MultiMethodExample is a routing type with a single resource, named "user", which has multiple resource methods. Handlers for the resource methods are provided as a record value, with field names matching the HTTP methods:

      resources =
        { user: { "GET": getUser
                , "DELETE": deleteUser
                }
        }

Content Negotiation

By specifying alternative content types for a method, 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 =
       "home"  := Resource (Get Home HTML)
  :<|> "users" := "users" :/ Resource (Get AllUsers (HTML :<|> JSON))
  :<|> "user"  := "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
<div>
  <h1>Users</h1>
  <ul>
    <li><a href="/users/1">John Paul Jones</a></li>
    <li><a href="/users/2">Tal Wilkenfeld</a></li>
    ...
  </ul>
</div>