https://github.com/cedlemo/ocaml-backend-rest-api-notes
https://github.com/cedlemo/ocaml-backend-rest-api-notes
Last synced: 5 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/cedlemo/ocaml-backend-rest-api-notes
- Owner: cedlemo
- Created: 2019-12-28T14:18:08.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2020-01-05T11:05:57.000Z (almost 6 years ago)
- Last Synced: 2025-06-02T22:01:49.177Z (7 months ago)
- Language: OCaml
- Size: 13.7 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Exploring REST API with OCaml
* [Introduction](#introduction)
* [Creating the backend](#creating-the-backend)
* [Dune project initialization](#dune-project-initialization)
* [A backend with the main httpaf example](#a-backend-with-the-main-httpaf-example)
* [A backend that handles GET POST PUT and DELETE http requests](#a-backend-that-handles-GET-POST-PUT-and-DELETE-http-request)
* [A backend that handles JSON data requests](#a-backedn-that-handles-json-data-requests)
* [A backend that uses a database]
* [Creating the React frontend]
* [From React to Reasonml]
* [Deploying with Docker]
## Introduction
the technical stack will be:
backend: https://github.com/inhabitedtype/httpaf
database:
frontend: react/reasonml
deployement: docker https://jaredforsyth.com/posts/deploying-native-reason-ocaml-with-now-sh/
the application will be a classical todo list.
## Creating the backend:
### Dune project initialization:
```
mkdir -p resume-backend/bin
cd resume-backend
touch bin/dune
touch bin/backend.ml
```
In the dune file:
```
(executable
(name backend)
)
```
in the backend.ml file:
```
let () =
print_endline "Hello, world!"
```
build and run:
```
$ dune build bin/backend.exe
Info: Creating file dune-project with this contents:
| (lang dune 1.11)
$ ll
total 12K
drwxr-xr-x 2 cedlemo cedlemo 4,0K 27 déc. 17:35 bin
drwxr-xr-x 3 cedlemo cedlemo 4,0K 27 déc. 17:35 _build
-rw-r--r-- 1 cedlemo cedlemo 17 27 déc. 17:35 dune-project
$ dune exec bin/backend.exe
Hello, world!
```
Import `httpaf` dependency and trying to build the main example:
int the *bin/dune* file
```
(executable
(name backend)
(library httpaf)
)
```
in the *bin/backend.ml* file
```
open Httpaf
module String = Caml.String
let invalid_request reqd status body =
(* Responses without an explicit length or transfer-encoding are
close-delimited. *)
let headers = Headers.of_list [ "Connection", "close" ] in
Reqd.respond_with_string reqd (Response.create ~headers status) body
;;
let request_handler reqd =
let { Request.meth; target; _ } = Reqd.request reqd in
match meth with
| `GET ->
begin match String.split_on_char '/' target with
| "" :: "hello" :: rest ->
let who =
match rest with
| [] -> "world"
| who :: _ -> who
in
let response_body = Printf.sprintf "Hello, %s!\n" who in
(* Specify the length of the response. *)
let headers =
Headers.of_list
[ "Content-length", string_of_int (String.length response_body) ]
in
Reqd.respond_with_string reqd (Response.create ~headers `OK) response_body
| _ ->
let response_body = Printf.sprintf "%S not found\n" target in
invalid_request reqd `Not_found response_body
end
| meth ->
let response_body =
Printf.sprintf "%s is not an allowed method\n" (Method.to_string meth)
in
invalid_request reqd `Method_not_allowed response_body
;;
```
```
dune build bin/backend.exe
```
### A backend with the main example:
In the first part, there is the main example of `httpaf`:
```ocaml
open Httpaf
open Base
open Lwt.Infix
open Httpaf_lwt_unix
module String = Caml.String
module Arg = Caml.Arg
let invalid_request reqd status body =
(* Responses without an explicit length or transfer-encoding are
close-delimited. *)
let headers = Headers.of_list [ "Connection", "close" ] in
Reqd.respond_with_string reqd (Response.create ~headers status) body
;;
let _request_handler reqd =
let { Request.meth; target; _ } = Reqd.request reqd in
match meth with
| `GET ->
begin match String.split_on_char '/' target with
| "" :: "hello" :: rest ->
let who =
match rest with
| [] -> "world"
| who :: _ -> who
in
let response_body = Printf.sprintf "Hello, %s!\n" who in
(* Specify the length of the response. *)
let headers =
Headers.of_list
[ "Content-length", Int.to_string (String.length response_body) ]
in
Reqd.respond_with_string reqd (Response.create ~headers `OK) response_body
| _ ->
let response_body = Printf.sprintf "%S not found\n" target in
invalid_request reqd `Not_found response_body
end
| meth ->
let response_body =
Printf.sprintf "%s is not an allowed method\n" (Method.to_string meth)
in
invalid_request reqd `Method_not_allowed response_body
;;
let request_handler (_: Unix.sockaddr) = _request_handler
let _error_handler ?request:_ error start_response =
let response_body = start_response Headers.empty in
begin match error with
| `Exn exn ->
Body.write_string response_body (Exn.to_string exn);
Body.write_string response_body "\n";
| #Status.standard as error ->
Body.write_string response_body (Status.default_reason_phrase error)
end;
Body.close_writer response_body
;;
let error_handler (_ : Unix.sockaddr) = _error_handler
```
then, we create an http server that listens to a port and dispatch the request to the `request_handler`:
```ocaml
let main port =
let listen_address = Unix.(ADDR_INET (inet_addr_loopback, port)) in
Lwt.async (fun () ->
Lwt_io.establish_server_with_client_socket
listen_address
(Server.create_connection_handler ~request_handler ~error_handler)
>|= fun _server ->
Stdio.printf "Starting server and listening at http://localhost:%d\n\n%!" port);
let forever, _ = Lwt.wait () in
Lwt_main.run forever
;;
let () =
let port = ref 8080 in
Arg.parse
["-p", Arg.Set_int port, " Listening port number (8080 by default)"]
ignore
"Echoes POST requests. Runs forever.";
main !port
;;
```
In order to build and launch the server:
```
dune clean
dune build bin/backend.exe
dune exec bin/backend.exe
Listening at http://localhost:8080
```
Just try to go to *http://localhost:8080/hello/toto* and observe :
```
Hello, toto!
```
See full sources in the directory: backend-1
### A backend that handles GET POST PUT and DELETE http requests
Now the backend will be splitted in two files:
- backend.ml: with the code that manages the web server
- lib.ml: with the code that handles the requests.
For the `GET` and `DELETE` requests, the server just returns a string with the type of the request and the url requested.
```ocaml
let response_body = Printf.sprintf "%s request on url %s\n" (Method.to_string meth) target in
let resp_headers = build_headers response_body in
Reqd.respond_with_string reqd (Response.create ~headers:resp_headers `OK) response_body
```
For the `POST` and `PUT` requests, the server will return the same data sent in the request.
```ocaml
let response =
let content_type =
match Headers.get headers "content-type" with
| None -> "application/octet-stream"
| Some x -> x
in
Response.create ~headers:(Headers.of_list ["content-type", content_type; "connection", "close"]) `OK
in
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response in
let rec on_read buffer ~off ~len =
Body.write_bigstring response_body buffer ~off ~len;
Body.schedule_read request_body ~on_eof ~on_read;
and on_eof () =
Body.close_writer response_body
in
Body.schedule_read request_body ~on_eof ~on_read
```
First, a response_body stream is created, then `Body.schedule_read` is called to read the request_body and write the data in the response_body. At `eof`, the call to `Body.close_writer` indicates that the response_body should be returned to the client.
*backend.ml*
```ocaml
let response =
let content_type =
match Headers.get headers "content-type" with
| None -> "application/octet-stream"
| Some x -> x
in
Response.create ~headers:(Headers.of_list ["content-type", content_type; "connection", "close"]) `OK
in
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response in
let rec on_read buffer ~off ~len =
Body.write_bigstring response_body buffer ~off ~len;
Body.schedule_read request_body ~on_eof ~on_read;
and on_eof () =
Body.close_writer response_body
in
Body.schedule_read request_body ~on_eof ~on_read
```
*lib.ml*
```ocaml
let response =
let content_type =
match Headers.get headers "content-type" with
| None -> "application/octet-stream"
| Some x -> x
in
Response.create ~headers:(Headers.of_list ["content-type", content_type; "connection", "close"]) `OK
in
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response in
let rec on_read buffer ~off ~len =
Body.write_bigstring response_body buffer ~off ~len;
Body.schedule_read request_body ~on_eof ~on_read;
and on_eof () =
Body.close_writer response_body
in
Body.schedule_read request_body ~on_eof ~on_read
```
Build and run :
```
$ dune build bin/backend.exe
$ dune exec bin/backend.exe
Starting server and listening at http://localhost:8080
```
test:
```
$ curl -i -X GET localhost:8080/toto
HTTP/1.1 200 OK
Content-length: 25
connection: close
GET request on url /toto
```
```
$ curl -i -X DELETE localhost:8080/toto/1
HTTP/1.1 200 OK
Content-length: 30
connection: close
DELETE request on url /toto/1
```
```
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"numberofsaves": "272"}' localhost:8080/toto
HTTP/1.1 200 OK
content-type: application/json
connection: close
{"numberofsaves": "272"}
```
```
$ curl -i -X PUT -H 'Content-Type: application/json' -d '{"numberofsaves": "272"}' localhost:8080/toto
HTTP/1.1 200 OK
content-type: application/json
connection: close
{"numberofsaves": "272"}
```
See full sources in the directory backend-2.
### A backend that handles JSON data requests.
The library `yojson` (https://github.com/ocaml-community/yojson) allows to manipulate json data from a string.
For the `PUT` and `POST` requests, data is passed in json format. In the server the data is just received as a string and should be verified and transformed into json.
```ocaml
let request_body = Reqd.request_body reqd in
let data = Buffer.create 1024 in
let rec on_read buffer ~off ~len =
let str = Bigstringaf.substring buffer ~off ~len in
let () = Buffer.add_string data str in
Body.schedule_read request_body ~on_eof ~on_read;
and on_eof () =
(* Get the JSON data and print it in the backend output *)
let json = (Buffer.sub data 0 (Buffer.length data)) |> Bytes.to_string |> Yojson.Basic.from_string in
Stdio.print_endline (Yojson.Basic.pretty_to_string json);
(* Return an OK response *)
let response_body = Printf.sprintf "%s request on url %s\n" (Method.to_string meth) target in
send_response response_body
in
Body.schedule_read request_body ~on_eof ~on_read
```
The `GET` request should return data into the json format.
```ocaml
| `GET -> let json_values =
`List [
`Assoc
[
("id", `String "1");
("name", `String "todo 1");
( "description", `String "do this, do that");
];
`Assoc
[
("id", `String "2");
("name", `String "todo 2");
( "description", `String "do this again, do that again");
]
]
in
let response_body = Yojson.Basic.to_string json_values in
send_response response_body
```
See full sources in the directory backend-3