
# RESTful JSON API using Eliom

<!--wodoc:aside class="concepts"-->**Concepts**

RESTful API<br/>PUT and DELETE services<!--wodoc:end-->

This tutorial will show you how to create a simple, yet complete, REST API using JSON as the serialization format.

To illustrate our example, let's say we want to give access to a database of locations storing a description and coordinates (latitude and longitude).

To be RESTful, our interface will comply with the following principles:

- URLs and GET params identify resources
- HTTP methods are used to define actions to perform (GET, POST, PUT, DELETE)
- GET action is safe (no side-effect)
- PUT and DELETE actions are idempotent
- Requests are stateless
With this in mind, our goal will be to implement CRUD (Create, Read, Update, Delete) functions to handle our resources. We want the following requests to be valid:

`GET http://localhost/` will return all available locations.

`GET http://localhost/ID` will return location associated to `ID`.

`POST http://localhost/ID` with content:

```json
{
  "description": "Paris",
  "coordinates": {
    "latitude": 48.8567,
    "longitude": 2.3508
  }
}
```
will store this location in the database.

`PUT http://localhost/ID`, with some content, will update the location associated to `ID`.

`DELETE http://localhost/ID` will delete the location associated to `ID`.


## Requirements

- eliom \>= 4\.0
- yojson
- ppx\_deriving
- ppx\_deriving\_yojson
You need some knowledge about Eliom to be able to fully understand this tutorial. It's not meant to be an Eliom introduction.

The following browser extensions are useful to manually test your REST APIs:

- [RESTClient](https://addons.mozilla.org/en-US/firefox/addon/restclient/) for Firefox \* [Postman](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm) for Chrome

## Data types

We start by defining our database types, that is to say the way we will represent our locations and associated information. Each location will be associated to a unique and arbitrary identifier, and will hold the following information: a description and coordinates (made of a latitude and a longitude).

We represent coordinates with decimal degrees, and use the `ppx_deriving_yojson` library to parse and generate JSON serialization of our types.

We use a dedicated `error` type returned when something is wrong with the request itself or with the processing of the request.

As for the database, we use a simple `Ocsipersist` table.

```ocaml
type coordinates = {
  latitude : float;
  longitude : float;
} [@@deriving yojson]

type location = {
  description : string option;
  coordinates : coordinates;
} [@@deriving yojson]

(* List of pairs (identifier * location) *)
type locations =
  (string * location) list
  [@@deriving yojson]

type error = {
  error_message : string;
} [@@deriving yojson]

let db : location Ocsipersist.table Lwt.t =
  Ocsipersist.Polymorphic.open_table "locations"
```
<!--wodoc:aside class="concept"-->**Concept: Syntax extensions**

In order to successfully build this tutorial's source code, your `Makefile.options` must contain the following settings:

```makefile
# OCamlfind packages for the server
SERVER_PACKAGES := ppx_deriving_yojson lwt_ppx js_of_ocaml-ppx.deriving
# OCamlfind packages for the client
CLIENT_PACKAGES := lwt_ppx ppx_deriving_yojson js_of_ocaml-ppx js_of_ocaml-ppx.deriving
```
This is due to the fact, that in the code snippet above we use a syntax extension provided by the `ppx_deriving_yojson` package. <!--wodoc:end-->


## Services definition

First, let's define common service parameters:

- The `path` of the API: it's the same for all services.
- The GET parameter, which is an optional identifier given as a URL suffix. We set it as optional so we can distinguish GET requests for all resources or a single one, and return a detailed error if the identifier is missing in POST, PUT and DELETE requests. An alternative would be to use two services at the same path (one with id and the other without).
```ocaml
let path = Eliom_service.Path [""]

let get_params =
  Eliom_parameter.(suffix (neopt (string "id")))
```
Next step is to define our API services. We define four of them with the same path, using the four HTTP methods at our disposal:

- GET method will be used to access the database, either all of it if no identifier is provided, or a single resource otherwise. An error will be returned if no resource matches the identifier.
- POST method will be used to create a new resource (or update it if it already exists). We set a single POST parameter: `Eliom_parameter.raw_post_data`, in order to retrieve raw JSON and bypass POST parameters decoding.
- PUT method will be used to update an existing resource. An error will be returned if no resource matches the identifier. We don't need to define POST parameter, PUT services takes `Eliom_parameter.raw_post_data` as default content.
- DELETE method will be used to delete an existing resource. An error will be returned if no resource matches the identifier.
```ocaml
let read_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Get get_params)
    ()

let create_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Post (get_params, Eliom_parameter.raw_post_data))
    ()

let update_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Put get_params)
    ()

let delete_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Delete get_params)
    ()
```

## Handlers

Let's start the handlers definition with a few helper values and functions used by handlers.

Since we use the low-level `Eliom_registration.String.send` function to send our response, we wrap it in three specialized functions: `send_json`, `send_error` and `send_success` (this one only send the 200 OK status code, without any content).

Another function helps us to check the received `Content-type` is the expected one, by matching it against a MIME type. In our example, we'll verify that we are receiving JSON.

The `read_raw_content` function retrieve at most `length` characters from the `raw_content` Ocsigen stream.

```ocaml
let json_mime_type = "application/json"

let send_json ~code json =
  Eliom_registration.String.send ~code (json, json_mime_type)

let send_error ~code error_message =
  let json = Yojson.Safe.to_string (error_to_yojson {error_message}) in
  send_json ~code json

let send_success () =
  Eliom_registration.String.send ~code:200 ("", "")

let check_content_type ~mime_type content_type =
  match content_type with
  | Some ((type_, subtype), _)
      when (type_ ^ "/" ^ subtype) = mime_type -> true
  | _ -> false

let read_raw_content ?(length = 4096) raw_content =
  let content_stream = Ocsigen_stream.get raw_content in
  Ocsigen_stream.string_of_stream length content_stream
```
Then we define our handlers in order to perform the required actions and return a response.

POST and PUT handlers will read raw body content as JSON and use `Yojson` to convert it to our types.

We use HTTP status codes in responses, with these meaning:

- 200 (OK): the request succeeded.
- 400 (Bad request): something is wrong with the request (missing parameter, parsing error...).
- 404 (Not found): no resource matches the provided identifier.
The GET handler either returns a single location if an identifier is provided, or a list of all existing locations otherwise.

```ocaml
let read_handler id_opt () =
  let%lwt db = db in
  match id_opt with
  | None ->
    Ocsipersist.Polymorphic.fold_step
      (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db []
    >>= fun locations ->
    let json = Yojson.Safe.to_string (locations_to_yojson locations) in
    send_json ~code:200 json
  | Some id ->
    catch
      (fun () ->
         Ocsipersist.Polymorphic.find db  id >>= fun location ->
         let json =
           Yojson.Safe.to_string (location_to_yojson location) in
         send_json ~code:200 json)
      (function
        | Not_found ->
          (* [id] hasn't been found, return a "Not found" message *)
          send_error ~code:404 ("Resource not found: " ^ id)
        | _ ->  send_error ~code:500 "Internal server error" )
```
Next, let's create a common function for POST and PUT handlers, which have a very similar behaviour. The only difference is that a PUT request with a non-existing identifier will fail (thus only accepting update requests, and rejecting creations), whereas the same request with POST method will succeed (a new location associated to the identifier will be created).

```ocaml
let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) =
  let%lwt db = db in
  if not (check_content_type ~mime_type:json_mime_type content_type) then
    send_error ~code:400 "Content-type is wrong, it must be JSON"
  else
    match id_opt, raw_content_opt with
    | None, _ ->
      send_error ~code:400 "Location identifier is missing"
    | _, None ->
      send_error ~code:400 "Body content is missing"
    | Some id, Some raw_content ->
      read_raw_content raw_content >>= fun location_str ->
      catch (fun () ->
          (if create then
            Lwt.return_unit
           else
            Ocsipersist.Polymorphic.find db id >>= fun _ -> Lwt.return_unit)
          >>= fun () ->
          let location =
            (let open Result in
            let loc_result = location_of_yojson (Yojson.Safe.from_string location_str) in
            match loc_result with
              Ok loc-> loc
            | Error _ -> raise Deriving_Yojson.Failed )  in
          Ocsipersist.Polymorphic.add db  id location >>= fun () ->
          send_success ())
      (function
        | Not_found ->
          send_error ~code:404 ("Location not found: " ^ id)
        | Deriving_Yojson.Failed ->
          send_error ~code:400 "Provided JSON is not valid"
        | _ -> send_error ~code:500 "Internal server error")

let create_handler id_opt content =
  edit_handler_aux ~create:true id_opt content

let update_handler id_opt content =
  edit_handler_aux ~create:false id_opt content
```
We need a fourth handler to delete locations:

```ocaml
let%lwt db = db in
  match id_opt with
  | None ->
    send_error ~code:400 "An id must be provided to delete a location"
  | Some id ->
    Ocsipersist.Polymorphic.remove db id >>= fun () ->
    send_success ()
```

## Services registration

Finally we register services with the `Eliom_registration.Any` module in order to have full control on the sent response. This way, we'll be able to send an appropriate HTTP status code depending on what happens during the request processing (parsing error, resource not found...), as seen above when defining handlers.

```ocaml
let () =
  Eliom_registration.Any.register read_service read_handler;
  Eliom_registration.Any.register create_service create_handler;
  Eliom_registration.Any.register update_service update_handler;
  Eliom_registration.Any.register delete_service delete_handler;
  ()
```

## Full source

```ocaml
open Lwt

(**** Data types ****)

type coordinates = {
  latitude : float;
  longitude : float;
} [@@deriving yojson]

type location = {
  description : string option;
  coordinates : coordinates;
} [@@deriving yojson]

(* List of pairs (identifier * location) *)
type locations =
  (string * location) list
   [@@deriving yojson]

type error = {
  error_message : string;
} [@@deriving yojson]

let db : location Ocsipersist.table Lwt.t =
  Ocsipersist.Polymorphic.open_table "locations"


(**** Services ****)

let path = Eliom_service.Path [""]

let get_params =
  Eliom_parameter.(suffix (neopt (string "id")))

let read_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Get get_params)
    ()

let create_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Post (get_params, Eliom_parameter.raw_post_data))
    ()

let update_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Put get_params)
    ()

let delete_service =
  Eliom_service.create
    ~path
    ~meth:(Eliom_service.Delete get_params)
    ()

(**** Handler helpers ****)

let json_mime_type = "application/json"

let send_json ~code json =
  Eliom_registration.String.send ~code (json, json_mime_type)

let send_error ~code error_message =
  let json = Yojson.Safe.to_string (error_to_yojson {error_message}) in
  send_json ~code json

let send_success () =
  Eliom_registration.String.send ~code:200 ("", "")

let check_content_type ~mime_type content_type =
  match content_type with
  | Some ((type_, subtype), _)
      when (type_ ^ "/" ^ subtype) = mime_type -> true
  | _ -> false

let read_raw_content ?(length = 4096) raw_content =
  let content_stream = Ocsigen_stream.get raw_content in
  Ocsigen_stream.string_of_stream length content_stream

(**** Handlers ****)

let read_handler id_opt () =
  let%lwt db = db in
  match id_opt with
  | None ->
    Ocsipersist.Polymorphic.fold_step
      (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db []
    >>= fun locations ->
    let json = Yojson.Safe.to_string (locations_to_yojson locations) in
    send_json ~code:200 json
  | Some id ->
    catch
      (fun () ->
         Ocsipersist.Polymorphic.find db  id >>= fun location ->
         let json =
           Yojson.Safe.to_string (location_to_yojson location) in
         send_json ~code:200 json)
      (function
        | Not_found ->
          (* [id] hasn't been found, return a "Not found" message *)
          send_error ~code:404 ("Resource not found: " ^ id)
        | _ -> assert false)

let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) =
  let%lwt db = db in
  if not (check_content_type ~mime_type:json_mime_type content_type) then
    send_error ~code:400 "Content-type is wrong, it must be JSON"
  else
    match id_opt, raw_content_opt with
    | None, _ ->
      send_error ~code:400 "Location identifier is missing"
    | _, None ->
      send_error ~code:400 "Body content is missing"
    | Some id, Some raw_content ->
      read_raw_content raw_content >>= fun location_str ->
      catch (fun () ->
          (if create then
            Lwt.return_unit
           else
            Ocsipersist.Polymorphic.find db id >>= fun _ -> Lwt.return_unit)
          >>= fun () ->
          let location =
            (let open Result in
            let loc_result = location_of_yojson (Yojson.Safe.from_string location_str) in
            (function
              Ok loc-> loc
            | Error _ -> raise Deriving_Yojson.Failed ) loc_result)  in
          Ocsipersist.Polymorphic.add db  id location >>= fun () ->
          send_success ())
      (function
        | Not_found ->
          send_error ~code:404 ("Location not found: " ^ id)
        | Deriving_Yojson.Failed ->
          send_error ~code:400 "Provided JSON is not valid"
        | _ -> assert false)

let create_handler id_opt content =
  edit_handler_aux ~create:true id_opt content

let update_handler id_opt content =
  edit_handler_aux ~create:false id_opt content

let delete_handler id_opt _ =
  let%lwt db = db in
  match id_opt with
  | None ->
    send_error ~code:400 "An id must be provided to delete a location"
  | Some id ->
    Ocsipersist.Polymorphic.remove db id >>= fun () ->
    send_success ()

(* Register services *)

let () =
  Eliom_registration.Any.register read_service read_handler;
  Eliom_registration.Any.register create_service create_handler;
  Eliom_registration.Any.register update_service update_handler;
  Eliom_registration.Any.register delete_service delete_handler;
  ()
```