1# Generating Javascript functions to query an API
3We will now see how **servant** lets you turn an API type into javascript
4functions that you can call to query a webservice.
6For this, we will consider a simple page divided in two parts. At the top, we
7will have a search box that lets us search in a list of Haskell books by
8author/title with a list of results that gets updated every time we enter or
9remove a character, while at the bottom we will be able to see the classical
10[probabilistic method to approximate
11pi](http://en.wikipedia.org/wiki/Approximations_of_%CF%80#Summing_a_circle.27s_area),
12using a webservice to get random points. Finally, we will serve an HTML file
13along with a couple of Javascript files, among which one that's automatically
14generated from the API type and which will provide ready-to-use functions to
17The source for this tutorial section is a literate haskell file, so first we
18need to have some language extensions and imports:
21{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
23{-# LANGUAGE OverloadedStrings #-}
24{-# LANGUAGE TypeOperators #-}
26module Javascript where
28import Control.Monad.IO.Class
31import Data.Text as T (Text)
32import Data.Text.IO as T (writeFile, readFile)
34import Language.Javascript.JQuery
36import Network.Wai.Handler.Warp
37import qualified Data.Text as T
43Now let's have the API type(s) and the accompanying datatypes.
46type API = "point" :> Get '[JSON] Point
47 :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book)
49type API' = API :<|> Raw
63mkSearch :: Text -> [a] -> Search a
66instance ToJSON a => ToJSON (Search a)
76book :: Text -> Text -> Int -> Book
80We need a "book database". For the purpose of this guide, let's restrict
81ourselves to the following books.
86 [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000
87 , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008
88 , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011
89 , book "Graham Hutton" "Programming in Haskell" 2007
90 , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013
91 , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998
95Now, given an optional search string `q`, we want to perform a case insensitive
96search in that list of books. We're obviously not going to try and implement
97the best possible algorithm, this is out of scope for this tutorial. The
98following simple linear scan will do, given how small our list is.
101searchBook :: Monad m => Maybe Text -> m (Search Book)
102searchBook Nothing = return (mkSearch "" books)
103searchBook (Just q) = return (mkSearch q books')
105 where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b)
106 || q' `T.isInfixOf` T.toLower (title b)
112We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y
113<= 1`. The code below uses
114[random](http://hackage.haskell.org/package/random)'s `System.Random`.
117randomPoint :: MonadIO m => m Point
118randomPoint = liftIO . getStdRandom $ \g ->
119 let (rx, g') = randomR (-1, 1) g
120 (ry, g'') = randomR (-1, 1) g'
121 in (Point rx ry, g'')
124If we add static file serving, our server is now complete.
137server' :: Server API'
139 :<|> serveDirectory "static"
142app = serve api' server'
148Why two different API types, proxies and servers though? Simply because we
149don't want to generate javascript functions for the `Raw` part of our API type,
150so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
152Very similarly to how one can derive haskell functions, we can derive the
153javascript with just a simple function call to `jsForAPI` from
158apiJS1 = jsForAPI api jquery
161This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
165var getPoint = function(onSuccess, onError)
175var getBooks = function(q, onSuccess, onError)
178 { url: '/books' + '?q=' + encodeURIComponent(q)
186We created a directory `static` that contains two static files: `index.html`,
187which is the entrypoint to our little web application; and `ui.js`, which
188contains some hand-written javascript. This javascript code assumes the two
189generated functions `getPoint` and `getBooks` in scope. Therefore we need to
190write the generated javascript into a file:
195 T.writeFile "static/api.js" apiJS1
196 jq <- T.readFile =<< Language.Javascript.JQuery.file
197 T.writeFile "static/jq.js" jq
200(We're also writing the jquery library into a file, as it's also used by
201`ui.js`.) `static/api.js` will be included in `index.html` and the two
202generated functions will therefore be available in `ui.js`.
204And we're good to go. You can start the `main` function of this file and go to
205`http://localhost:8000/`. Start typing in the name of one of the authors in our
206database or part of a book title, and check out how long it takes to
207approximate pi using the method mentioned above.
211Instead of calling `jquery`, you can call its variant `jqueryWith`.
212Here are the type definitions
215jquery :: JavaScriptGenerator
216jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator
219The `CommonGeneratorOptions` will let you define different behaviors to
220change how functions are generated. Here is the definition of currently
224data CommonGeneratorOptions = CommonGeneratorOptions
226 -- | function generating function names
227 functionNameBuilder :: FunctionName -> Text
228 -- | name used when a user want to send the request body (to let you redefine it)
229 , requestBody :: Text
230 -- | name of the callback parameter when the request was successful
231 , successCallback :: Text
232 -- | name of the callback parameter when the request reported an error
233 , errorCallback :: Text
234 -- | namespace on which we define the js function (empty mean local var)
236 -- | a prefix that should be prepended to the URL in the generated JS
241This pattern is available with all supported backends, and default values are provided.
245If you don't use JQuery for your application, you can reduce your
246dependencies to simply use the `XMLHttpRequest` object from the standard API.
248Use the same code as before but simply replace the previous `apiJS` with
253apiJS2 = jsForAPI api vanillaJS
256The rest is *completely* unchanged.
258The output file is a bit different, but it has the same parameters,
263var getPoint = function(onSuccess, onError)
265 var xhr = new XMLHttpRequest();
266 xhr.open('GET', '/point', true);
267 xhr.setRequestHeader(\"Accept\",\"application/json\");
268 xhr.onreadystatechange = function (e) {
269 if (xhr.readyState == 4) {
270 if (xhr.status == 204 || xhr.status == 205) {
272 } else if (xhr.status >= 200 && xhr.status < 300) {
273 var value = JSON.parse(xhr.responseText);
276 var value = JSON.parse(xhr.responseText);
284var getBooks = function(q, onSuccess, onError)
286 var xhr = new XMLHttpRequest();
287 xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
288 xhr.setRequestHeader(\"Accept\",\"application/json\");
289 xhr.onreadystatechange = function (e) {
290 if (xhr.readyState == 4) {
291 if (xhr.status == 204 || xhr.status == 205) {
293 } else if (xhr.status >= 200 && xhr.status < 300) {
294 var value = JSON.parse(xhr.responseText);
297 var value = JSON.parse(xhr.responseText);
308And that's all, your web service can of course be accessible from those
309two clients at the same time!
315If you use Axios library for your application, we support that too!
317Use the same code as before but simply replace the previous `apiJS` with
322apiJS3 = jsForAPI api $ axios defAxiosOptions
325The rest is *completely* unchanged.
327The output file is a bit different,
332var getPoint = function()
334 return axios({ url: '/point'
341var getBooks = function(q)
343 return axios({ url: '/books' + '?q=' + encodeURIComponent(q)
350**Caution:** In order to support the promise style of the API, there are no onSuccess
351nor onError callback functions.
353### Defining Axios configuration
355Axios lets you define a 'configuration' to determine the behavior of the
356program when the AJAX request is sent.
358We mapped this into a configuration
361data AxiosOptions = AxiosOptions
362 { -- | indicates whether or not cross-site Access-Control requests
363 -- should be made using credentials
364 withCredentials :: !Bool
365 -- | the name of the cookie to use as a value for xsrf token
366 , xsrfCookieName :: !(Maybe Text)
367 -- | the name of the header to use as a value for xsrf token
368 , xsrfHeaderName :: !(Maybe Text)
376You can apply the same procedure as with `vanillaJS` and `jquery`, and
377generate top level functions.
379The difference is that `angular` Generator always takes an argument.
383apiJS4 = jsForAPI api $ angular defAngularOptions
386The generated code will be a bit different than previous generators. An extra
387argument `$http` will be added to let Angular magical Dependency Injector
390**Caution:** In order to support the promise style of the API, there are no onSuccess
391nor onError callback functions.
396var getPoint = function($http)
406var getBooks = function($http, q)
409 { url: '/books' + '?q=' + encodeURIComponent(q)
416You can then build your controllers easily
420app.controller("MyController", function($http) {
421 this.getPoint = getPoint($http)
422 .success(/* Do something */)
423 .error(/* Report error */);
425 this.getPoint = getBooks($http, q)
426 .success(/* Do something */)
427 .error(/* Report error */);
433You can also generate automatically a service to wrap the whole API as
434a single Angular service:
437app.service('MyService', function($http) {
439 postCounter: function()
446 getCounter: function()
449 { url: '/books' + '?q=' + encodeURIComponent(q), true);
457To do so, you just have to use an alternate generator.
461apiJS5 = jsForAPI api $ angularService defAngularOptions
464Again, it is possible to customize some portions with the options.
467data AngularOptions = AngularOptions
468 { -- | When generating code with wrapInService, name of the service to generate, default is 'app'
470 , -- | beginning of the service definition
471 prologue :: Text -> Text -> Text
472 , -- | end of the service definition
477# Custom function name builder
479Servant comes with three name builders included:
481- camelCase (the default)
485Keeping the JQuery as an example, let's see the impact:
489apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase }
492This `Text` contains 2 Javascript functions:
497var get_point = function(onSuccess, onError)
507var get_books = function(q, onSuccess, onError)
510 { url: '/books' + '?q=' + encodeURIComponent(q)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list