Structuring APIs

In this recipe, we will see a few simple ways to structure your APIs by splitting them up into smaller “sub-APIs” or by sharing common structure between different parts. Let’s start with the usual throat clearing.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
import Data.Aeson
import GHC.Generics
import GHC.TypeLits
import Network.Wai.Handler.Warp
import Servant

Our application will consist of three different “sub-APIs”, with a few endpoints in each of them. Our global API is defined as follows.

type API = FactoringAPI
      :<|> SimpleAPI "users" User UserId
      :<|> SimpleAPI "products" Product ProductId

We simply join the three different parts with :<|>, as if each sub-API was just a simple endpoint. The first part, FactoringAPI, shows how we can “factor out” combinators that are common to several endpoints, just like we turn a * b + a * c into a * (b + c) in algebra.

-- Two endpoints:
--   - GET /x/<some 'Int'>[?y=<some 'Int'>]
--   - POST /x/<some 'Int'>
type FactoringAPI =
  "x" :> Capture "x" Int :>
      (    QueryParam "y" Int :> Get '[JSON] Int
      :<|>                       Post '[JSON] Int
      )

{- this is equivalent to:

type FactoringAPI' =
  "x" :> Capture "x" Int :> QueryParam "y" Int :> Get '[JSON] Int :<|>
  "x" :> Capture "x" Int :> Post '[JSON] Int
-}

You can see that both endpoints start with a static path fragment, /"x", then capture some arbitrary Int until they finally differ. Now, this also has an effect on the server for such an API, and its type in particular. While the server for FactoringAPI' would be made of a function of type Int -> Maybe Int -> Handler Int and a function of type Int -> Handler Int glued with :<|>, a server for FactoringAPI (without the ') reflects the “factorisation” and therefore, Server FactoringAPI is Int -> (Maybe Int -> Handler Int :<|> Handler Int). That is, the server must be a function that takes an Int (the Capture) and returns two values glued with :<|>, one of type Maybe Int -> Handler Int and the other of type Handler Int. Let’s provide such a server implementation, with those “nested types”.

Tip: you can load this module in ghci and ask for the concrete type that Server FactoringAPI “resolves to” by typing :kind! Server FactoringAPI.

factoringServer :: Server FactoringAPI
factoringServer x = getXY :<|> postX

  where getXY Nothing  = return x
        getXY (Just y) = return (x + y)

        postX = return (x - 1)

If you want to avoid the “nested types” and the need to manually dispatch the arguments (like x above) to the different request handlers, and would just like to be able to declare the API type as above but pretending that the Capture is not factored out, that every combinator is “distributed” (i.e that all endpoints are specified like FactoringAPI' above), then you should look at flatten from the servant-flatten package.

Next come the two sub-APIs defined in terms of this SimpleAPI type, but with different parameters. That type is just a good old Haskell type synonym that abstracts away a pretty common structure in web services, where you have:

  • one endpoint for listing a bunch of entities of some type
  • one endpoint for accessing the entity with a given identifier
  • one endpoint for creating a new entity

There are many variants on this theme (endpoints for deleting, paginated listings, etc). The simple definition below reproduces such a structure, but instead of picking concrete types for the entities and their identifiers, we simply let the user of the type decide, by making those types parameters of SimpleAPI. While we’re at it, we’ll put all our endpoints under a common prefix that we also take as a parameter.

-- Three endpoints:
--   - GET /<name>
--   - GET /<name>/<some 'i'>
--   - POST /<name>
type SimpleAPI (name :: Symbol) a i = name :>
  (                         Get '[JSON] [a]
  :<|> Capture "id" i    :> Get '[JSON] a
  :<|> ReqBody '[JSON] a :> Post '[JSON] NoContent
  )

Symbol is the kind of type-level strings, which is what servant uses for representing static path fragments. We can even provide a little helper function for creating a server for that API given one handler for each endpoint as arguments.

simpleServer
  :: Handler [a]
  -> (i -> Handler a)
  -> (a -> Handler NoContent)
  -> Server (SimpleAPI name a i)
simpleServer listAs getA postA =
  listAs :<|> getA :<|> postA

{- you could alternatively provide such a definition
   but with the handlers running in another monad,
   or even an arbitrary one!

simpleAPIServer
  :: m [a]
  -> (i -> m a)
  -> (a -> m NoContent)
  -> Server (SimpleAPI name a i) m
simpleAPIServer listAs getA postA =
  listAs :<|> getA :<|> postA

   and use 'hoistServer' on the result of `simpleAPIServer`
   applied to your handlers right before you call `serve`.
-}

We can use this to define servers for the user and product related sections of the API.

userServer :: Server (SimpleAPI "users" User UserId)
userServer = simpleServer
  (return [])
  (\userid -> return $
      if userid == 0
      then User "john" 64
      else User "everybody else" 10
  )
  (\_user -> return NoContent)

productServer :: Server (SimpleAPI "products" Product ProductId)
productServer = simpleServer
  (return [])
  (\_productid -> return $ Product "Great stuff")
  (\_product -> return NoContent)

Finally, some dummy types and the serving part.

type UserId = Int

data User = User { username :: String, age :: Int }
  deriving Generic

instance FromJSON User
instance ToJSON   User

type ProductId = Int

data Product = Product { productname :: String }
  deriving Generic

instance FromJSON Product
instance ToJSON   Product

api :: Proxy API
api = Proxy

main :: IO ()
main = run 8080 . serve api $
  factoringServer :<|> userServer :<|> productServer

This program is available as a cabal project here.