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.