Skip to content

Instantly share code, notes, and snippets.

@gera-cameron
Created August 5, 2020 16:58
Show Gist options
  • Save gera-cameron/1d8c28511e7e80e05d5f93defd977acf to your computer and use it in GitHub Desktop.
Save gera-cameron/1d8c28511e7e80e05d5f93defd977acf to your computer and use it in GitHub Desktop.

Servant Talk 8/5/2020 by Cameron

  • What is Servant and how is it different than Happstack?

    • Per servant.dev, servant is a set of packages for declaring web APIs at the type-level.

    • Per Cameron, super powerful and type safe, yet fairly confusing way to write APIs.

    • It is different than happstack in quite a few ways, but the biggest difference to us is the ability to generate swagger documentation as well as how type safe it is. A large place where Happstack fell short for us was the fact that we didn't have type safety in our routes. What do you mean by that?

          -- sample route from happstack
          dirs "api/consumer-web/v2/certificate/" 
            $ path $ \userExternalId -> 
                path $ \courseExternalId -> get $ ConsumerWeb.Certificate.downloadCertificate userExternalId courseExternalId

      We could easily mix up the userExternalId and the courseExternalId's order if we aren't careful, creating a runtime error. With Servant, however, we must represent our API with a type that then cooresponds to a list of handlers.

          -- sample route from servant
          type Route
            = Urza.ConsumerWeb
            Urza.:> "api"
            Urza.:> "consumer-web"
            Urza.:> "v2"
            Urza.:> "certificate"
            Urza.:> Urza.Capture "user-id" Gluon.UserExternalId
            Urza.:> Urza.Capture "course-id" Gluon.CourseExternalId
            Urza.:> Urza.ReqBody '[Urza.JSON] Input
            Urza.:> Urza.RequiredAuth Urza.ConsumerWeb
            Urza.:> Servant.Get '[Servant.JSON] Output

      As you can see the Servant route is quite verbose, but it requires us to define our types before runtime so we don't get those runtime exceptions.

  • Pro/Con List vs Happstack

    Happstack Pros Happstack Cons Servant Pros Servant Cons
    Generally less overhead to get started No out of the box documentation support Out of the box documentation support More complexity when setting up because the errors are atrocious
    Easy to ready and understand route composition Hard to test the handlers Full type safety and API is represented by a type Lots of confusing operators
  • How Bauble is used and why

    • Bauble is a type wrapper that provides ToSchema and To/FromJSON instance using the generic deriving (with our prefix removing). This removes the requirement for needing to derive these instances in every handler file.
  • Are there any patterns unique to our servant implementation?

    • Just like in Happstack, we are much more verbose and don't really "share" routes. In the docs for Servant, they tend to create the first key (So in our route above it would be api) and then create all the other routes under that key. Also we do authentication differently because the support for JWT auth is kinda rough in Servant, but other than that I can't really find differences with other implementations.
  • Any discussions you can find on or ways you can think of to separate servant routes from their documentation, then later weave them together appropriately.

    Reason being if that were possible we could have both now concise routes and be less afraid of adding tons of extra documentation. Not sure if I'd be a fan of that or not, but trying it crossed my mind.

  • How do you dispatch on a servant route's content types like:

    • Urza.:> Urza.Get '[Urza.JSON, Servant.CSV] Output

      In your handler how can you say "if the accept type is json, do this, else if the accept type is OctetStream, do this"?

      Edit: related roughly implying "not that you should": haskell-servant/servant#1294

    • So what happens here is that the server listens for the "Accept" header and will encode the response based on that and the available encodings ('[Urza.JSON, Servant.CSV]). If no Accept header is sent, then the server will respond with the first type in the list of encodings. So we can say the "default" is Urza.JSON and that's what we are currently reflecting in the docs. This may change depending on what we end up doing with the CSV encoding.

    • As far as accessing that in the handler itself is a little fuzzy. You can either add it to the Route as a Header (will return a maybe) and attempt to put it into an ADT type or you can use the Servant.getHeaders fn and find it that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment