7. Building haskell client for third-party APIΒΆ

WebApi framework could be used to build haskell clients for existing API services. All you have to do is

  • Define the routes (as types)
  • Write the contract for the API service.

To demonstrate, we’ve chosen Uber API as the third party API service and picked the two most commonly used endpoints in Uber API

Since we have already discussed what a contract is under the Quick start section in detail we can jump straight to our example.

Lets first define the type for the API service, call it UberApi and types for our routes. (get time estimate and request a ride ).

data UberApi

-- pieces of a route are seperated using ':/'
type TimeEstimateR = "estimates" :/ "time"
-- If the route has only one piece, we use 'Static' constructor to build it.
type RequestRideR  = Static "requests"

Now lets define what methods (GET, POST etc.) can be used on these routes. For this we need to define WebApi instance for our service UberApi .

instance WebApi UberApi where
    type Apis UberApi =
        '[ Route '[GET] TimeEstimateR
         , Route '[POST] RequestRideR

So far, we have defined the routes and the methods associated with them. We are yet to define how the requests and responses will look for these two end-points (contract).

We’ll start with the TimeEstimateR route. As defined in the Uber API doc , GET request for TimeEstimateR takes the user’s current latitude, longitude, product_id (if any) as query parameters and return back a result containig a list of TimeEstimate (rides nearby along with time estimates). And this is how we represent the query and the response as data types.

-- query data type
data TimeParams = TimeParams
    { start_latitude   :: Double
    , start_longitude  :: Double
    , product_id       :: Maybe Text
    } deriving (Generic)

-- response data type
newtype Times = Times { times :: [TimeEstimate] }
   deriving (Show, Generic)

-- We prefix each field with 't_' to prevent name clashes.
-- It will be removed during deserialization
data TimeEstimate = TimeEstimate
   { t_product_id   :: Text
   , t_display_name :: Text
   , t_estimate     :: Int
   } deriving (Show, Generic)

instance ApiContract UberApi GET TimeEstimateR where
    type HeaderIn   GET TimeEstimateR = Token
    type QueryParam GET TimeEstimateR = TimeParams
    type ApiOut     GET TimeEstimateR = Times

As request to Uber API requires an Authorization header, we include that in our contract for each route. The data type Token used in the header is defined here

There is still one piece missing though. Serialization/ de-serialization of request/response data types. To do that, we need to give FromJSON instance for our response and ToParam instance for the query param datatype.

instance ToParam 'QueryParam TimeParams
instance FromJSON Times
instance FromJSON TimeEstimate where
    parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 2 }

Similarly we can write contract for the other routes too. You can find the full contract here .

And that’s it! By simply defining a contract we have built a Haskell client for Uber API. The code below shows how to make the API calls.

-- To get the time estimates, we can write our main function as:
main :: IO ()
main = do
    manager <- newManager tlsManagerSettings
    let timeQuery = TimeParams 12.9760 80.2212 Nothing
        cSettings = ClientSettings "https://sandbox-api.uber.com/v1" manager
        auth'     = OAuthToken "<Your-Access-Token-here>"
        auth      = OAuth auth'

    times' <- client cSettings (Request () timeQuery () () auth () () :: WebApi.Request GET TimeEstimateR)
    -- remaining main code

We use client function to send the request. It takes ClientSettings and Request as input and gives us the Response . If you see the Request pattern synonym, we need to give it all the params, headers etc. to construct a Request . So for a particular route, the params which we declare in the contract are filled with the declared datatypes and the rest defaults to () unit

When the endpoint gives a response back, WebApi deserializes it into Response . Lets write a function to handle the response.

let responseHandler res fn = case res of
       Success _ res' _ _   -> fn res'
       Failure err          -> print "Request failed :("

We have successfully made a request and now can handle the response with responseHandler. If the previous request (to get time estimate) was succesful, lets book the nearest ride with our second route.

  responseHandler times' $ \times -> do
      let rideId = getNearestRideId times
          reqQuery = defRideReqParams { product_id = Just rideId, start_place_id = Just "work", end_place_d = Just "home" }
          ridereq   = client cSettings (Request () () () () auth' () reqQuery :: WebApi.Request POST RequestRideR)
      rideInfo' <- ridereq
      responseHandler rideInfo' $ \rideInfo -> do
          putStrLn "You have successfully booked a ride. Yay!"
          putStrLn $ "Ride Status: " ++ unpack (status rideInfo)
  return ()
  getNearestRideId (Times xs) = t_product_id . head . sortBy (comparing t_estimate) $ xs

And that’s it! We now have our haskell client. Using the same contract you can also generate a mock server

You can find the full uber client library for haskell here .