JSON Web Service

Let’s create a very simple web service: it takes a JSON request and returns a JSON response. We’re going to write the server in WAI/Warp, and the client in http-conduit. We’ll be using aeson for JSON parsing and rendering. We could also write the server in Yesod itself, but for such a simple example, the extra features of Yesod don’t add much.

Server

WAI uses the conduit package to handle streaming request bodies, and efficiently generates responses using blaze-builder. aeson uses attoparsec for parsing; by using attoparsec-conduit we get easy interoperability with WAI. This plays out as:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Control.Exception (SomeException)
  3. import Control.Exception.Lifted (handle)
  4. import Control.Monad.IO.Class (liftIO)
  5. import Data.Aeson (Value, encode, object, (.=))
  6. import Data.Aeson.Parser (json)
  7. import Data.ByteString (ByteString)
  8. import Data.Conduit (($$))
  9. import Data.Conduit.Attoparsec (sinkParser)
  10. import Network.HTTP.Types (status200, status400)
  11. import Network.Wai (Application, Response, responseLBS)
  12. import Network.Wai.Conduit (sourceRequestBody)
  13. import Network.Wai.Handler.Warp (run)
  14. main :: IO ()
  15. main = run 3000 app
  16. app :: Application
  17. app req sendResponse = handle (sendResponse . invalidJson) $ do
  18. value <- sourceRequestBody req $$ sinkParser json
  19. newValue <- liftIO $ modValue value
  20. sendResponse $ responseLBS
  21. status200
  22. [("Content-Type", "application/json")]
  23. $ encode newValue
  24. invalidJson :: SomeException -> Response
  25. invalidJson ex = responseLBS
  26. status400
  27. [("Content-Type", "application/json")]
  28. $ encode $ object
  29. [ ("message" .= show ex)
  30. ]
  31. -- Application-specific logic would go here.
  32. modValue :: Value -> IO Value
  33. modValue = return

Client

http-conduit was written as a companion to WAI. It too uses conduit and blaze-builder pervasively, meaning we once again get easy interop with aeson. A few extra comments for those not familiar with http-conduit:

  • A Manager is present to keep track of open connections, so that multiple requests to the same server use the same connection. You usually want to use the getGlobalManager function to get the global connection manager.

  • We need to know the size of our request body, which can’t be determined directly from a Builder. Instead, we convert the Builder into a lazy ByteString and take the size from there.

  • There are a number of different functions for initiating a request. We use http, which allows us to directly access the data stream. There are other higher level functions (such as httpLbs) that let you ignore the issues of sources and get the entire body directly.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Control.Monad.IO.Class (liftIO)
  3. import Data.Aeson (Value (Object, String))
  4. import Data.Aeson (encode, object, (.=))
  5. import Data.Aeson.Parser (json)
  6. import Data.Conduit (($$+-))
  7. import Data.Conduit.Attoparsec (sinkParser)
  8. import Network.HTTP.Conduit (RequestBody (RequestBodyLBS),
  9. Response (..), http, method, parseUrl,
  10. requestBody, getGlobalManager)
  11. main :: IO ()
  12. main = do
  13. manager <- getGlobalManager
  14. value <- liftIO makeValue
  15. -- We need to know the size of the request body, so we convert to a
  16. -- ByteString
  17. let valueBS = encode value
  18. req' <- liftIO $ parseUrl "http://localhost:3000/"
  19. let req = req' { method = "POST", requestBody = RequestBodyLBS valueBS }
  20. res <- http req manager
  21. resValue <- responseBody res $$+- sinkParser json
  22. liftIO $ handleResponse resValue
  23. -- Application-specific function to make the request value
  24. makeValue :: IO Value
  25. makeValue = return $ object
  26. [ ("foo" .= ("bar" :: String))
  27. ]
  28. -- Application-specific function to handle the response from the server
  29. handleResponse :: Value -> IO ()
  30. handleResponse = print