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 :<|> serveDirectoryFileServer "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.
152The `EmptyAPI` combinator needs no special treatment as it generates no
153Javascript functions: an empty API has no endpoints to access.
155Very similarly to how one can derive haskell functions, we can derive the
156javascript with just a simple function call to `jsForAPI` from
161apiJS1 = jsForAPI api jquery
164This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks':
168var getPoint = function(onSuccess, onError)
178var getBooks = function(q, onSuccess, onError)
181 { url: '/books' + '?q=' + encodeURIComponent(q)
189We created a directory `static` that contains two static files: `index.html`,
190which is the entrypoint to our little web application; and `ui.js`, which
191contains some hand-written javascript. This javascript code assumes the two
192generated functions `getPoint` and `getBooks` in scope. Therefore we need to
193write the generated javascript into a file:
198 T.writeFile "static/api.js" apiJS1
199 jq <- T.readFile =<< Language.Javascript.JQuery.file
200 T.writeFile "static/jq.js" jq
203(We're also writing the jquery library into a file, as it's also used by
204`ui.js`.) `static/api.js` will be included in `index.html` and the two
205generated functions will therefore be available in `ui.js`.
207And we're good to go. You can start the `main` function of this file and go to
208`http://localhost:8000/`. Start typing in the name of one of the authors in our
209database or part of a book title, and check out how long it takes to
210approximate pi using the method mentioned above.
214Instead of calling `jquery`, you can call its variant `jqueryWith`.
215Here are the type definitions
218jquery :: JavaScriptGenerator
219jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator
222The `CommonGeneratorOptions` will let you define different behaviors to
223change how functions are generated. Here is the definition of currently
227data CommonGeneratorOptions = CommonGeneratorOptions
229 -- | function generating function names
230 functionNameBuilder :: FunctionName -> Text
231 -- | name used when a user want to send the request body (to let you redefine it)
232 , requestBody :: Text
233 -- | name of the callback parameter when the request was successful
234 , successCallback :: Text
235 -- | name of the callback parameter when the request reported an error
236 , errorCallback :: Text
237 -- | namespace on which we define the js function (empty mean local var)
239 -- | a prefix that should be prepended to the URL in the generated JS
244This pattern is available with all supported backends, and default values are provided.
248If you don't use JQuery for your application, you can reduce your
249dependencies to simply use the `XMLHttpRequest` object from the standard API.
251Use the same code as before but simply replace the previous `apiJS` with
256apiJS2 = jsForAPI api vanillaJS
259The rest is *completely* unchanged.
261The output file is a bit different, but it has the same parameters,
266var getPoint = function(onSuccess, onError)
268 var xhr = new XMLHttpRequest();
269 xhr.open('GET', '/point', true);
270 xhr.setRequestHeader(\"Accept\",\"application/json\");
271 xhr.onreadystatechange = function (e) {
272 if (xhr.readyState == 4) {
273 if (xhr.status == 204 || xhr.status == 205) {
275 } else if (xhr.status >= 200 && xhr.status < 300) {
276 var value = JSON.parse(xhr.responseText);
279 var value = JSON.parse(xhr.responseText);
287var getBooks = function(q, onSuccess, onError)
289 var xhr = new XMLHttpRequest();
290 xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true);
291 xhr.setRequestHeader(\"Accept\",\"application/json\");
292 xhr.onreadystatechange = function (e) {
293 if (xhr.readyState == 4) {
294 if (xhr.status == 204 || xhr.status == 205) {
296 } else if (xhr.status >= 200 && xhr.status < 300) {
297 var value = JSON.parse(xhr.responseText);
300 var value = JSON.parse(xhr.responseText);
311And that's all, your web service can of course be accessible from those
312two clients at the same time!
318If you use Axios library for your application, we support that too!
320Use the same code as before but simply replace the previous `apiJS` with
325apiJS3 = jsForAPI api $ axios defAxiosOptions
328The rest is *completely* unchanged.
330The output file is a bit different,
335var getPoint = function()
337 return axios({ url: '/point'
344var getBooks = function(q)
346 return axios({ url: '/books' + '?q=' + encodeURIComponent(q)
353**Caution:** In order to support the promise style of the API, there are no onSuccess
354nor onError callback functions.
356### Defining Axios configuration
358Axios lets you define a 'configuration' to determine the behavior of the
359program when the AJAX request is sent.
361We mapped this into a configuration
364data AxiosOptions = AxiosOptions
365 { -- | indicates whether or not cross-site Access-Control requests
366 -- should be made using credentials
367 withCredentials :: !Bool
368 -- | the name of the cookie to use as a value for xsrf token
369 , xsrfCookieName :: !(Maybe Text)
370 -- | the name of the header to use as a value for xsrf token
371 , xsrfHeaderName :: !(Maybe Text)
379You can apply the same procedure as with `vanillaJS` and `jquery`, and
380generate top level functions.
382The difference is that `angular` Generator always takes an argument.
386apiJS4 = jsForAPI api $ angular defAngularOptions
389The generated code will be a bit different than previous generators. An extra
390argument `$http` will be added to let Angular magical Dependency Injector
393**Caution:** In order to support the promise style of the API, there are no onSuccess
394nor onError callback functions.
399var getPoint = function($http)
409var getBooks = function($http, q)
412 { url: '/books' + '?q=' + encodeURIComponent(q)
419You can then build your controllers easily
423app.controller("MyController", function($http) {
424 this.getPoint = getPoint($http)
425 .success(/* Do something */)
426 .error(/* Report error */);
428 this.getPoint = getBooks($http, q)
429 .success(/* Do something */)
430 .error(/* Report error */);
436You can also generate automatically a service to wrap the whole API as
437a single Angular service:
440app.service('MyService', function($http) {
442 postCounter: function()
449 getCounter: function()
452 { url: '/books' + '?q=' + encodeURIComponent(q), true);
460To do so, you just have to use an alternate generator.
464apiJS5 = jsForAPI api $ angularService defAngularOptions
467Again, it is possible to customize some portions with the options.
470data AngularOptions = AngularOptions
471 { -- | When generating code with wrapInService, name of the service to generate, default is 'app'
473 , -- | beginning of the service definition
474 prologue :: Text -> Text -> Text
475 , -- | end of the service definition
480# Custom function name builder
482Servant comes with three name builders included:
484- camelCase (the default)
488Keeping the JQuery as an example, let's see the impact:
492apiJS6 = jsForAPI api $ jqueryWith defCommonGeneratorOptions { functionNameBuilder= snakeCase }
495This `Text` contains 2 Javascript functions:
500var get_point = function(onSuccess, onError)
510var get_books = function(q, onSuccess, onError)
513 { 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