Writing a haskell client for a third party API

Introduction to WebApi

In this blog we’ll talk about building a Haskell client for any 3rd party API service using WebApi. WebApi is a Haskell library that also lets you write web API services, generate API console interface and a mock server (mocks requests and responses).

You can read more about it here.

Getting started

To demonstrate, we’ve chosen Uber API as the third party API service and picked the two most commonly used endpoints in Uber API – get time estimate and request a ride. The first endpoint gets the time estimate for nearby rides. The second one lets us request a ride.

To write an API client using WebApi all you have to do is define the API contract. A contract is a list of end-points in your API service and the description of each API endpoint. We define what goes in (Eg Query params, Form params) and what comes out (the response) of each API endpoint. We will be seeing more of this later.

Lets first define a type for the API service and call it UberApi.

data UberApi
Next, we’ll define the routes for the end-points (get time estimate and request a ride).

-- 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.

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 (contract) how the requests and responses will look for these two end-points.

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 containing 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)

We can now use these data types in our contract. So the contract goes like:

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 TimeParams 'QueryParam
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)

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 successful, 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 ()
where
getNearestRideId (Times xs) = t_product_id . head . sortBy (comparing t_estimate) $ xs

Using the same contract we can also generate API console and a mock server. We’ll talk about these in the upcoming posts.

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