Intro🔗
Hreq is a high-level easy to use type-driven HTTP client library inspired by Servant-Client. Hreq provides an alternative approach to type-safe construction and interpretation of API endpoints for Http client requests.
Hreq should feel very familiar to anyone who has worked with Servant-Server or Servant-Client while feeling more lightweight and minimal.
Find library source code in the link below.
Motivation🔗
Hreq was motivated by the simplicity and ease of use of Req and the type-driven elegance of Servant-Client. I envisioned Hreq as the best possible compromise of both worlds.
The Servant client library was first and foremost designed as a solution to generate API client functions for pre-defined Servant server API structures. So it shines when used in that context. However, this doesn’t mean it doesn’t work well in isolation; it certainly does albeit at the cost of some boiler-plate.
Hreq, on the other hand, is designed for more general-purpose use. Its approach is thus similar to the one found in the Req library or some of the HTTP client libraries in mainstream programming languages. Hreq’s interface is, therefore, more straightforward and less verbose while maintaining good type-level expressiveness and ease of use.
Implementation🔗
One of Hreq’s key features is the flexibility of the API endpoint definitions and ease of interpretation. These features are made possible thanks to the ability to emulate dependent type like language features within modern Haskell via type families, GADTs and other advanced language extensions.
The library core functionality is provided by these two type classes:
The HasRequest class which interprets API endpoints into a
Request
data structure.The HasResponse class which declares the desired output from an HTTP response.
Hreq and Servant client feature comparison🔗
Hreq has a lot of similarities and differences with servant client, so it’s imperative I list some of the prominent ones.
Hreq’s API structures are more Kind restricted, enabling more type correctness and more straightforward formulation of type-level functions. A drawback of this approach is that Hreq is less extensible than servant-client.
Hreq provides a default HTTP client manager such that one doesn’t have to think about manager configuration. This is in stark contrast with Servant-Client where you have to offer one. It’s also possible to over-ride the provided default manager.
Hreq provides type synonyms for common API type combinators, therefore, making API endpoint definitions much shorter for some cases.
In Hreq API types are used directly within API functions via Type Application. While in servant-client, API endpoint types are used to create API querying functions. API endpoint types hence have a more first-class treatment in Hreq than in Servant client.
In Servant-client, valid responses must have a status code between 200 and 300. In Hreq one can configure a range for valid status codes via the HTTP config with 200 to 300 as the default.
In Hreq, API Request component arguments are provided to API functions through a Heterogeneous list. While in Servant client, arguments are provided to newly auto-created API functions.
Hreq has an inbuilt retry mechanism via the Retry package that enables retrying of network requests on connection failure. Network IO actions are prone to temporary failures that warrant retrying of the original action. On the other hand, servant-client doesn’t support a retry mechanism.
Servant-client has a native streaming backend via
SourceT
and supports other external streaming backends such as Conduit,Pipes
andMachines
. On the other hand, Hreq currently supportsstreaming
only viaConduit
. Other streaming backends can easily be added depending on community interest.Hreq provides pattern synonyms that make the creation of
BaseUrls
more concise and less error-prone. For example, theHttpDomain
andHttpsDomain
synonyms create base URLs from a provideddomain
. TheHttpUrl
andHttpsUrl
create base URLs from a provideddomain
andpath
. If one needs to provide a customport
number, they can fall back to directly using theBaseUrl
constructor.
Usage Example🔗
The same code sample can be found in the Example module on Github with the necessary language extensions.
Assume we are making requests against a hypothetical HTTP service providing a JSON user management API.
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import Control.Monad.IO.Class (liftIO)
import GHC.Generics (Generic)
import Hreq.Client
data User = User
{ name :: Text
, age :: Int
} deriving (Show, Generic, FromJSON, ToJSON)
-- User service API URL
baseUrl :: BaseUrl
baseUrl = HttpUrl "example.com" "user"
Simple Get request🔗
Make a Get request obtaining a User
by a specified user-name
at http://example.com/user/:userName
getUserByName :: RunClient m => Text -> m User
getUserByName userName = hreq @(Capture Text :> GetJson User) (userName :. Empty)
The Capture Text :> GetJson User
type within getUserByName
is an API endpoint type definition.
The API type definition in this instance demands that a heterogeneous list containing a Text
value is supplied to the hreq
function.
userName :. Empty
forms the required heterogeneous list value for the hreq
function. Finally, the API type states that we will obtain a JSON User
response output.
Simple Post request🔗
Make a Post request with Json User data for a request body returning a Json User response at http://example.com/user
createUser :: RunClient m => User -> m User
createUser user = hreq @(JsonBody User :> PostJson User) (user :. Empty)
Get Request with QueryFlag🔗
Make a Get request obtaining all users at API endpoint http://example.com/user/all?old
getAllUsers :: RunClient m => m [User]
getAllUsers = hreq @("all" :> QueryFlag "old" :> GetJson [User]) Empty
Running api endpoint functions🔗
In the main function; the API endpoint functions run within theHreq
monad. The Hreq monad is an instance of the RunClient
class and MonadIO
class.
main :: IO ()
main = runHreq baseUrl $ do
reqUser <- getUserByName "allan"
createdUser <- createUser newUser
allUsers <- getAllUsers
-- Delete users with age equal to 20
hreq @(Capture Int :> EmptyResponse DELETE) (20 :. Empty)
-- do something with API data
liftIO $ print (reqUser, createdUser, allUsers)
where
newUser :: User
newUser = User "allan" 12
More Examples🔗
More examples can be found on hackage and within library tests. An example showcasing streaming support via conduit can be found within the streaming package’s readme file.
Conclusion🔗
I hope you get to enjoy hreq. Please reach out through the project’s GitHub issue tracker if you come across any issues. Happy coding.
Acknowledgment🔗
Many thanks to Dmitrii Kovanikov and Alvin Kato for having reviewed my blog post and given me helpful suggestions.
Published on: November 12, 2019
My blog is hosted on Github. If you would like to leave a comment or report a problem please feel free to leave one there.