{"id":15724291,"url":"https://github.com/cedlemo/ocaml-backend-rest-api-notes","last_synced_at":"2025-07-27T12:07:49.650Z","repository":{"id":144939715,"uuid":"230619520","full_name":"cedlemo/ocaml-backend-rest-api-notes","owner":"cedlemo","description":null,"archived":false,"fork":false,"pushed_at":"2020-01-05T11:05:57.000Z","size":14,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-02T22:01:49.177Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"OCaml","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cedlemo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-28T14:18:08.000Z","updated_at":"2020-01-05T11:05:59.000Z","dependencies_parsed_at":"2023-04-04T21:02:29.824Z","dependency_job_id":null,"html_url":"https://github.com/cedlemo/ocaml-backend-rest-api-notes","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/cedlemo/ocaml-backend-rest-api-notes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cedlemo%2Focaml-backend-rest-api-notes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cedlemo%2Focaml-backend-rest-api-notes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cedlemo%2Focaml-backend-rest-api-notes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cedlemo%2Focaml-backend-rest-api-notes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cedlemo","download_url":"https://codeload.github.com/cedlemo/ocaml-backend-rest-api-notes/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cedlemo%2Focaml-backend-rest-api-notes/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267355138,"owners_count":24073915,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-27T02:00:11.917Z","response_time":82,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-03T22:16:01.259Z","updated_at":"2025-07-27T12:07:49.606Z","avatar_url":"https://github.com/cedlemo.png","language":"OCaml","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Exploring REST API with OCaml\n\n* [Introduction](#introduction)\n* [Creating the backend](#creating-the-backend)\n  * [Dune project initialization](#dune-project-initialization)\n  * [A backend with the main httpaf example](#a-backend-with-the-main-httpaf-example)\n  * [A backend that handles GET POST PUT and DELETE http requests](#a-backend-that-handles-GET-POST-PUT-and-DELETE-http-request)\n  * [A backend that handles JSON data requests](#a-backedn-that-handles-json-data-requests)\n  * [A backend that uses a database]\n* [Creating the React frontend]\n* [From React to Reasonml]\n* [Deploying with Docker]\n\n## Introduction\nthe technical stack will be:\n\nbackend: https://github.com/inhabitedtype/httpaf\ndatabase:\nfrontend: react/reasonml\ndeployement: docker https://jaredforsyth.com/posts/deploying-native-reason-ocaml-with-now-sh/\n\nthe application will be a classical todo list.\n\n## Creating the backend:\n\n### Dune project initialization:\n\n```\nmkdir -p resume-backend/bin\ncd resume-backend\ntouch bin/dune\ntouch bin/backend.ml\n```\n\nIn the dune file:\n\n```\n(executable\n (name backend)\n)\n```\n\nin the backend.ml file:\n\n```\nlet () =\n  print_endline \"Hello, world!\"\n```\n\nbuild and run:\n\n```\n$ dune build bin/backend.exe\n  Info: Creating file dune-project with this contents:\n  | (lang dune 1.11)\n$ ll\n  total 12K\n  drwxr-xr-x 2 cedlemo cedlemo 4,0K 27 déc.  17:35 bin\n  drwxr-xr-x 3 cedlemo cedlemo 4,0K 27 déc.  17:35 _build\n  -rw-r--r-- 1 cedlemo cedlemo   17 27 déc.  17:35 dune-project\n$ dune exec bin/backend.exe\n  Hello, world!\n```\n\nImport `httpaf` dependency and trying to build the main example:\n\nint the *bin/dune* file\n\n```\n(executable\n (name backend)\n (library httpaf)\n)\n```\n\nin the *bin/backend.ml* file\n```\nopen Httpaf\nmodule String = Caml.String\n\nlet invalid_request reqd status body =\n  (* Responses without an explicit length or transfer-encoding are\n     close-delimited. *)\n  let headers = Headers.of_list [ \"Connection\", \"close\" ] in\n  Reqd.respond_with_string reqd (Response.create ~headers status) body\n;;\n\nlet request_handler reqd =\n  let { Request.meth; target; _ } = Reqd.request reqd in\n  match meth with\n  | `GET -\u003e\n    begin match String.split_on_char '/' target with\n    | \"\" :: \"hello\" :: rest -\u003e\n      let who =\n        match rest with\n        | [] -\u003e \"world\"\n        | who :: _ -\u003e who\n      in\n      let response_body = Printf.sprintf \"Hello, %s!\\n\" who in\n      (* Specify the length of the response. *)\n      let headers =\n        Headers.of_list\n          [ \"Content-length\", string_of_int (String.length response_body) ]\n      in\n      Reqd.respond_with_string reqd (Response.create ~headers `OK) response_body\n    | _ -\u003e\n      let response_body = Printf.sprintf \"%S not found\\n\" target in\n      invalid_request reqd `Not_found response_body\n    end\n  | meth -\u003e\n    let response_body =\n      Printf.sprintf \"%s is not an allowed method\\n\" (Method.to_string meth)\n    in\n    invalid_request reqd `Method_not_allowed response_body\n;;\n```\n\n```\ndune build bin/backend.exe\n```\n\n### A backend with the main example:\n\nIn the first part, there is the main example of `httpaf`:\n\n```ocaml\nopen Httpaf\nopen Base\nopen Lwt.Infix\nopen Httpaf_lwt_unix\n\nmodule String = Caml.String\nmodule Arg = Caml.Arg\n\nlet invalid_request reqd status body =\n  (* Responses without an explicit length or transfer-encoding are\n     close-delimited. *)\n  let headers = Headers.of_list [ \"Connection\", \"close\" ] in\n  Reqd.respond_with_string reqd (Response.create ~headers status) body\n;;\n\nlet _request_handler reqd =\n  let { Request.meth; target; _ } = Reqd.request reqd in\n  match meth with\n  | `GET -\u003e\n    begin match String.split_on_char '/' target with\n    | \"\" :: \"hello\" :: rest -\u003e\n      let who =\n        match rest with\n        | [] -\u003e \"world\"\n        | who :: _ -\u003e who\n      in\n      let response_body = Printf.sprintf \"Hello, %s!\\n\" who in\n      (* Specify the length of the response. *)\n      let headers =\n        Headers.of_list\n          [ \"Content-length\", Int.to_string (String.length response_body) ]\n      in\n      Reqd.respond_with_string reqd (Response.create ~headers `OK) response_body\n    | _ -\u003e\n      let response_body = Printf.sprintf \"%S not found\\n\" target in\n      invalid_request reqd `Not_found response_body\n    end\n  | meth -\u003e\n    let response_body =\n      Printf.sprintf \"%s is not an allowed method\\n\" (Method.to_string meth)\n    in\n    invalid_request reqd `Method_not_allowed response_body\n;;\n\nlet request_handler (_: Unix.sockaddr) = _request_handler\n\nlet _error_handler ?request:_ error start_response =\n  let response_body = start_response Headers.empty in\n  begin match error with\n    | `Exn exn -\u003e\n      Body.write_string response_body (Exn.to_string exn);\n      Body.write_string response_body \"\\n\";\n    | #Status.standard as error -\u003e\n      Body.write_string response_body (Status.default_reason_phrase error)\n  end;\n  Body.close_writer response_body\n;;\n\nlet error_handler (_ : Unix.sockaddr) = _error_handler\n```\n\nthen, we create an http server that listens to a port and dispatch the request to the `request_handler`:\n\n```ocaml\nlet main port =\n  let listen_address = Unix.(ADDR_INET (inet_addr_loopback, port)) in\n  Lwt.async (fun () -\u003e\n    Lwt_io.establish_server_with_client_socket\n      listen_address\n      (Server.create_connection_handler ~request_handler ~error_handler)\n    \u003e|= fun _server -\u003e\n      Stdio.printf \"Starting server and listening at http://localhost:%d\\n\\n%!\" port);\n  let forever, _ = Lwt.wait () in\n  Lwt_main.run forever\n;;\n\nlet () =\n  let port = ref 8080 in\n  Arg.parse\n    [\"-p\", Arg.Set_int port, \" Listening port number (8080 by default)\"]\n    ignore\n    \"Echoes POST requests. Runs forever.\";\n  main !port\n;;\n```\n\nIn order to build and launch the server:\n\n```\ndune clean\ndune build bin/backend.exe\ndune exec bin/backend.exe\nListening at http://localhost:8080\n```\n\nJust try to go to *http://localhost:8080/hello/toto* and observe :\n```\nHello, toto!\n```\n\nSee full sources in the directory: backend-1\n\n### A backend that handles GET POST PUT and DELETE http requests\n\nNow the backend will be splitted in two files:\n- backend.ml: with the code that manages the web server\n- lib.ml: with the code that handles the requests.\n\nFor the `GET` and `DELETE` requests, the server just returns a string with the type of the request and the url requested.\n\n```ocaml\n    let response_body = Printf.sprintf \"%s request on url %s\\n\" (Method.to_string meth) target in\n    let resp_headers = build_headers response_body in\n    Reqd.respond_with_string reqd (Response.create ~headers:resp_headers `OK) response_body\n```\n\nFor the `POST` and `PUT` requests, the server will return the same data sent in the request.\n\n```ocaml\n    let response =\n        let content_type =\n          match Headers.get headers \"content-type\" with\n          | None   -\u003e \"application/octet-stream\"\n          | Some x -\u003e x\n        in\n        Response.create ~headers:(Headers.of_list [\"content-type\", content_type; \"connection\", \"close\"]) `OK\n    in\n    let request_body  = Reqd.request_body reqd in\n    let response_body = Reqd.respond_with_streaming reqd response in\n    let rec on_read buffer ~off ~len =\n      Body.write_bigstring response_body buffer ~off ~len;\n      Body.schedule_read request_body ~on_eof ~on_read;\n    and on_eof () =\n      Body.close_writer response_body\n    in\n    Body.schedule_read request_body ~on_eof ~on_read\n\n```\n\nFirst, 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.\n\n*backend.ml*\n\n```ocaml\n    let response =\n        let content_type =\n          match Headers.get headers \"content-type\" with\n          | None   -\u003e \"application/octet-stream\"\n          | Some x -\u003e x\n        in\n        Response.create ~headers:(Headers.of_list [\"content-type\", content_type; \"connection\", \"close\"]) `OK\n    in\n    let request_body  = Reqd.request_body reqd in\n    let response_body = Reqd.respond_with_streaming reqd response in\n    let rec on_read buffer ~off ~len =\n      Body.write_bigstring response_body buffer ~off ~len;\n      Body.schedule_read request_body ~on_eof ~on_read;\n    and on_eof () =\n      Body.close_writer response_body\n    in\n    Body.schedule_read request_body ~on_eof ~on_read\n```\n\n*lib.ml*\n\n```ocaml\n    let response =\n        let content_type =\n          match Headers.get headers \"content-type\" with\n          | None   -\u003e \"application/octet-stream\"\n          | Some x -\u003e x\n        in\n        Response.create ~headers:(Headers.of_list [\"content-type\", content_type; \"connection\", \"close\"]) `OK\n    in\n    let request_body  = Reqd.request_body reqd in\n    let response_body = Reqd.respond_with_streaming reqd response in\n    let rec on_read buffer ~off ~len =\n      Body.write_bigstring response_body buffer ~off ~len;\n      Body.schedule_read request_body ~on_eof ~on_read;\n    and on_eof () =\n      Body.close_writer response_body\n    in\n    Body.schedule_read request_body ~on_eof ~on_read\n\n```\n\nBuild and run :\n\n```\n$ dune build bin/backend.exe\n$ dune exec bin/backend.exe\nStarting server and listening at http://localhost:8080\n\n```\n\ntest:\n\n```\n$ curl -i -X GET localhost:8080/toto\nHTTP/1.1 200 OK\nContent-length: 25\nconnection: close\n\nGET request on url /toto\n```\n\n```\n$ curl -i -X DELETE localhost:8080/toto/1\nHTTP/1.1 200 OK\nContent-length: 30\nconnection: close\n\nDELETE request on url /toto/1\n```\n\n```\n$ curl -i -X POST -H 'Content-Type: application/json' -d '{\"numberofsaves\": \"272\"}' localhost:8080/toto\nHTTP/1.1 200 OK\ncontent-type: application/json\nconnection: close\n\n{\"numberofsaves\": \"272\"}\n```\n\n```\n$ curl -i -X PUT -H 'Content-Type: application/json' -d '{\"numberofsaves\": \"272\"}' localhost:8080/toto\nHTTP/1.1 200 OK\ncontent-type: application/json\nconnection: close\n\n{\"numberofsaves\": \"272\"}\n```\n\nSee full sources in the directory backend-2.\n\n### A backend that handles JSON data requests.\n\nThe library `yojson` (https://github.com/ocaml-community/yojson) allows to manipulate json data from a string.\n\nFor 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.\n\n```ocaml\n    let request_body  = Reqd.request_body reqd in\n    let data = Buffer.create 1024 in\n    let rec on_read buffer ~off ~len =\n      let str = Bigstringaf.substring buffer ~off ~len in\n      let () = Buffer.add_string data str in\n      Body.schedule_read request_body ~on_eof ~on_read;\n    and on_eof () =\n      (* Get the JSON data and print it in the backend output *)\n      let json = (Buffer.sub data 0 (Buffer.length data)) |\u003e Bytes.to_string  |\u003e Yojson.Basic.from_string in\n      Stdio.print_endline (Yojson.Basic.pretty_to_string json);\n      (* Return an OK response *)\n      let response_body = Printf.sprintf \"%s request on url %s\\n\" (Method.to_string meth) target in\n      send_response response_body\n    in\n    Body.schedule_read request_body ~on_eof ~on_read\n```\n\nThe `GET` request should return data into the json format.\n\n```ocaml\n  | `GET -\u003e let json_values =\n              `List [\n                `Assoc\n                [\n                    (\"id\", `String \"1\");\n                    (\"name\", `String \"todo 1\");\n                    ( \"description\", `String \"do this, do that\");\n                ];\n                `Assoc\n                [\n                    (\"id\", `String \"2\");\n                    (\"name\", `String \"todo 2\");\n                    ( \"description\", `String \"do this again, do that again\");\n                ]\n              ]\n    in\n    let response_body = Yojson.Basic.to_string json_values in\n    send_response response_body\n```\nSee full sources in the directory backend-3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcedlemo%2Focaml-backend-rest-api-notes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcedlemo%2Focaml-backend-rest-api-notes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcedlemo%2Focaml-backend-rest-api-notes/lists"}