I want to create a backend for a simple REST API in OCaml. For this I use the libs httpaf and yojson. I have something that works but I would like some reviews.
Here is the structure of my little server:
bin/
| - backend.ml
| - lib.ml
| - dune
- dune:
(executable
(name backend)
(libraries httpaf httpaf-lwt-unix base stdio lwt lwt.unix yojson)
)
- backend.ml
open Base
open Lwt.Infix
open Httpaf_lwt_unix
module String = Caml.String
module Arg = Caml.Arg
let request_handler (_: Unix.sockaddr) = Lib.request_handler
let error_handler (_ : Unix.sockaddr) = Lib.error_handler
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
- lib.ml
open Httpaf
open Base
module String = Caml.String
let invalid_request reqd status body =
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; headers; _ } = Reqd.request reqd in
let build_headers response_body =
Headers.of_list
[ "Content-length", Int.to_string (String.length response_body);
"Content-Type", "application/json";
"connection", "close"]
in
match meth with
| `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
let resp_headers = build_headers response_body in
Reqd.respond_with_string reqd (Response.create ~headers:resp_headers `OK) response_body
| `POST | `PUT ->
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 () =
let json = (Buffer.sub data 0 (Buffer.length data)) |> Bytes.to_string |> Yojson.Basic.from_string in
let () = Stdio.print_endline (Yojson.Basic.pretty_to_string json) in
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
in
Body.schedule_read request_body ~on_eof ~on_read
| `DELETE ->
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
| 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 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
I build build, run and test my server with:
dune build bin/backend.exe --profile=release
dune exec bin/backend.exe --profile=release
curl -i -X GET localhost:8080/toto
curl -i -X POST -H "Content-Type: application/json" -d some_random_json localhost:8080/toto
In the lib.ml file, for the POST and PUT request, I read the data from the request body and transform it in json, is it the right way to do it?
How can I improve this ?