
OCaml and RESTful JSON API using Eliom
Hello, Habr! I present to you the translation of the RESTful JSON API using Eliom .
This tutorial shows how to create a simple but complete REST API using JSON as the serialization format.
To illustrate our example, suppose we want to provide 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:
With this in mind, our goal will be to implement the CRUD functions (Create, Read, Update, Delete) to process our resources. We want the following requests to be valid:
GET http: // localhost / will return all available locations.
GET http: // localhost / ID will return the location associated with the ID.
POST http: // localhost / ID with content:
save this location in the database.
PUT http: // localhost / ID, with some content, will update the location associated with the identifier.
DELETE http: // localhost / ID will delete the location associated with the ID.
It is assumed that you are already familiar with Eliom , this is necessary to fully understand the tutorial. This tutorial is not an introduction to Eliom.
The following browser extensions may be useful for manually checking the REST API:
We start by defining our database types, that is, how we will represent our locations and related information. Each location will be associated with a unique and arbitrary identifier, and will also contain the following information: description and coordinates (consisting of latitude and longitude).
We represent coordinates with decimal degrees and use the deriving-yojson library to parse and serialize our types in JSON.
We use the highlighted error type that is returned when something is wrong with the request or with the processing of the request.
As for the database, we use a simple Ocsipersist table .
First, let's define common maintenance options:
The next step is to define our service APIs. We define four of them with the same path using the four HTTP methods at our disposal:
Let's start defining handlers with a few helper values and functions used by handlers.
Since we use the low-level function Eliom_registration.String.send to send our response, we transfer it to three specialized functions: send_json, send_error and send_success (this only sends a 200 OK status code without any content).
Another function helps us verify that the resulting content type is expected by matching it with the MIME type. In our example, we verify that we get JSON.
The read_raw_content function retrieves the specified or standard length number of characters from the Ocsigen raw_content stream.
Then we define our handlers to perform the necessary actions and return a response.
POST and PUT handlers will read the contents of the original content in JSON and use Yojson to convert it to our types.
In the responses, we use HTTP status codes, with values:
The GET handler either returns a single location if an identifier is provided, otherwise a list of all existing locations.
Then let's create a generic function for the POST and PUT handlers, which have very similar behavior. The only difference is that a PUT request with a non-existent identifier will return an error (thus, it will only accept update requests and reject creation requests), while the same request with the POST method will succeed (a new location will be created associated with with identifier).
To remove locations, you need a fourth handler:
Finally, we register services using the Eliom_registration.Any module to have full control over the response being sent. Thus, we will be able to send the appropriate HTTP status code depending on what happens during the processing of the request (parsing error, resource not found ...), as shown above when defining handlers.
I would like the OCaml community to grow bigger and grow, and the language develops faster, the language is good, and in some places even better than the mainstream languages, here are a few of its advantages: it is assembled in native, its syntax is quite concise and understandable (not right away, but how it is easier for me than Haskell, but in general it’s tasteful), it’s also a rather convenient type system and good OOP of course. If this translation was useful to someone or made me look at OCaml and its ecosystem, try it, then I can do more translations or author articles. Please report errors in PM.
PS:
Introductory articles about OCaml and Ocsigen on Habr, with which perhaps it is worth familiarizing with beginners:
but of course it’s better to familiarize yourself with the official manuals, because the articles are 6-7 years old, you can certainly get some basics from them (and taking into account the sluggish development of the language, the probability of extracting basic knowledge and not undermining tends to be 100%), but I I can’t guarantee that everything is right there at the moment, especially in the article about Oscigen. All good and enjoyable development.
This tutorial shows how to create a simple but complete REST API using JSON as the serialization format.
To illustrate our example, suppose we want to provide 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 parameters define resources
- HTTP methods (GET, POST, PUT, DELETE) are used to define actions
- GET action is safe (no side effects)
- PUT and DELETE actions are idempotent
- Requests are stateless (in the period between client requests, no information about the state of the client is stored on the server)
With this in mind, our goal will be to implement the CRUD functions (Create, Read, Update, Delete) to process our resources. We want the following requests to be valid:
GET http: // localhost / will return all available locations.
GET http: // localhost / ID will return the location associated with the ID.
POST http: // localhost / ID with content:
{
"description": "Paris",
"coordinates": {
"latitude": 48.8567,
"longitude": 2.3508
}
}
save this location in the database.
PUT http: // localhost / ID, with some content, will update the location associated with the identifier.
DELETE http: // localhost / ID will delete the location associated with the ID.
Dependencies
- eliom > = 4.0
- yojson
- deriving-yojson
It is assumed that you are already familiar with Eliom , this is necessary to fully understand the tutorial. This tutorial is not an introduction to Eliom.
The following browser extensions may be useful for manually checking the REST API:
Data types
We start by defining our database types, that is, how we will represent our locations and related information. Each location will be associated with a unique and arbitrary identifier, and will also contain the following information: description and coordinates (consisting of latitude and longitude).
We represent coordinates with decimal degrees and use the deriving-yojson library to parse and serialize our types in JSON.
We use the highlighted error type that is returned when something is wrong with the request or with the processing of the request.
As for the database, we use a simple Ocsipersist table .
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 =
Ocsipersist.open_table "locations"
Service Definition
First, let's define common maintenance options:
- path API: same for all services.
- The GET parameter, which is an optional identifier specified as a suffix for the URL. We set it as optional so that we can distinguish between GET requests for one or all resources and return a detailed error if the identifier is not in the POST, PUT, and DELETE requests. An alternative would be to use two services on the same path (one with id and one without).
let path = []
let get_params =
Eliom_parameter.(suffix (neopt (string "id")))
The next step is to define our service APIs. We define four of them with the same path using the four HTTP methods at our disposal:
- The GET method will be used to access the database, for any of the resources, if no identifier is specified, or only for a single resource. If the resource does not match the identifier, an error will be returned.
- The POST method will be used to create a new resource (or update it if it already exists). We set one POST parameter: Eliom_parameter.raw_post_data to get raw JSON and bypass the parameter lock after decoding.
- The PUT method will be used to update an existing resource. If the resource does not match the identifier, an error will be returned. We do not need to define the POST parameter, the PUT services accept the value Eliom_parameter.raw_post_data as the default content.
- The DELETE method will be used to delete an existing resource. If the resource does not match the identifier, an error will be returned.
let read_service =
Eliom_service.Http.service
~path
~get_params
()
let create_service =
Eliom_service.Http.post_service
~fallback:read_service
~post_params:Eliom_parameter.raw_post_data
()
let update_service =
Eliom_service.Http.put_service
~path
~get_params
()
let delete_service =
Eliom_service.Http.delete_service
~path
~get_params
()
Handlers
Let's start defining handlers with a few helper values and functions used by handlers.
Since we use the low-level function Eliom_registration.String.send to send our response, we transfer it to three specialized functions: send_json, send_error and send_success (this only sends a 200 OK status code without any content).
Another function helps us verify that the resulting content type is expected by matching it with the MIME type. In our example, we verify that we get JSON.
The read_raw_content function retrieves the specified or standard length number of characters from the Ocsigen raw_content stream.
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.to_string { 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 to perform the necessary actions and return a response.
POST and PUT handlers will read the contents of the original content in JSON and use Yojson to convert it to our types.
In the responses, we use HTTP status codes, with values:
- 200 (OK): The request completed successfully.
- 400 (invalid request): something is wrong with the request (missing parameter, parsing error ...).
- 404 (Not Found): The resource does not match the provided identifier.
The GET handler either returns a single location if an identifier is provided, otherwise a list of all existing locations.
let read_handler id_opt () =
match id_opt with
| None ->
Ocsipersist.fold_step
(fun id loc acc -> Lwt.return ((id, loc) :: acc)) db []
>>= fun locations ->
let json = Yojson.to_string locations in
send_json ~code:200 json
| Some id ->
catch (fun () ->
Ocsipersist.find db id >>= fun location ->
let json = Yojson.to_string 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))
Then let's create a generic function for the POST and PUT handlers, which have very similar behavior. The only difference is that a PUT request with a non-existent identifier will return an error (thus, it will only accept update requests and reject creation requests), while the same request with the POST method will succeed (a new location will be created associated with with identifier).
let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) =
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.find db id >>= fun _ -> Lwt.return_unit)
>>= fun () ->
let location = Yojson.from_string location_str in
Ocsipersist.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")
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
To remove locations, you need a fourth handler:
let delete_handler id_opt _ =
match id_opt with
| None ->
send_error ~code:400 "An id must be provided to delete a location"
| Some id ->
Ocsipersist.remove db id >>= fun () ->
send_success ()
Service Registration
Finally, we register services using the Eliom_registration.Any module to have full control over the response being sent. Thus, we will be able to send the appropriate HTTP status code depending on what happens during the processing of the request (parsing error, resource not found ...), as shown above when defining handlers.
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
All that we got as a result
Source: RESTful JSON API using Eliom
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 =
Ocsipersist.open_table "locations"
(**** Services ****)
let path = []
let get_params =
Eliom_parameter.(suffix (neopt (string "id")))
let read_service =
Eliom_service.Http.service
~path
~get_params
()
let create_service =
Eliom_service.Http.post_service
~fallback:read_service
~post_params:Eliom_parameter.raw_post_data
()
let update_service =
Eliom_service.Http.put_service
~path
~get_params
()
let delete_service =
Eliom_service.Http.delete_service
~path
~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.to_string { 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 () =
match id_opt with
| None ->
Ocsipersist.fold_step
(fun id loc acc -> Lwt.return ((id, loc) :: acc)) db []
>>= fun locations ->
let json = Yojson.to_string locations in
send_json ~code:200 json
| Some id ->
catch (fun () ->
Ocsipersist.find db id >>= fun location ->
let json = Yojson.to_string 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))
let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) =
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.find db id >>= fun _ -> Lwt.return_unit)
>>= fun () ->
let location = Yojson.from_string location_str in
Ocsipersist.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")
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 _ =
match id_opt with
| None ->
send_error ~code:400 "An id must be provided to delete a location"
| Some id ->
Ocsipersist.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;
()
Source: RESTful JSON API using Eliom
From translator
I would like the OCaml community to grow bigger and grow, and the language develops faster, the language is good, and in some places even better than the mainstream languages, here are a few of its advantages: it is assembled in native, its syntax is quite concise and understandable (not right away, but how it is easier for me than Haskell, but in general it’s tasteful), it’s also a rather convenient type system and good OOP of course. If this translation was useful to someone or made me look at OCaml and its ecosystem, try it, then I can do more translations or author articles. Please report errors in PM.
PS:
Introductory articles about OCaml and Ocsigen on Habr, with which perhaps it is worth familiarizing with beginners:
- Introduction to OCaml: The Basics [1]
- Introduction to OCaml: Structure of Programs at OCaml [2]
- Introduction to OCaml: Data Types and Mapping [3]
- Introduction to OCaml: Null Pointers, Statements and Warnings [4]
- Dynamic applications with Ocsigen or Yoba returns
but of course it’s better to familiarize yourself with the official manuals, because the articles are 6-7 years old, you can certainly get some basics from them (and taking into account the sluggish development of the language, the probability of extracting basic knowledge and not undermining tends to be 100%), but I I can’t guarantee that everything is right there at the moment, especially in the article about Oscigen. All good and enjoyable development.