やまどり
OCamlとReactでWebアプリを作ろう

OCamlとReactでWebアプリを作ろう

OCamlのWebフレームワークDreamとReactを組み合わせ、API通信ができるシンプルなWebアプリを作ります。

Share on XShare on Facebook

はじめに

OCamlは、強力な型システムと型推論を備えた関数型プログラミング言語です。コンパイル時に多くの間違いを検出できるため、安全性を重視したWebアプリケーション開発にも向いています。

今回は、次の技術を使って簡単なWebアプリケーションを作ります。

  • バックエンド:OCaml
  • Webフレームワーク:Dream
  • ビルドツール:Dune
  • フロントエンド:React
  • フロントエンド開発環境:Vite

Dreamは、少ない記述でHTTPサーバーやAPIを実装できるOCaml向けWebフレームワークです。TLSやWebSocketなどにも対応しています。

フロントエンドでは、React公式ドキュメントでもビルドツールの選択肢として紹介されているViteを使用します。

今回作るのは、ReactからOCamlのAPIへアクセスし、サーバーから受け取ったメッセージを画面に表示するアプリケーションです。

完成するアプリケーション

ブラウザでページを開くと、Reactから次のAPIが呼び出されます。

GET /api/hello

OCamlサーバーは、次のようなJSONを返します。

{
  "message": "Hello from OCaml!"
}

Reactは受け取ったメッセージを画面に表示します。

最終的なディレクトリ構成は次のとおりです。

ocaml-react-app/
├── backend/
│   ├── dune-project
│   ├── dune
│   └── server.ml
└── frontend/
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── App.jsx
        └── main.jsx

手順

OCamlの開発環境を準備する

OCamlのパッケージ管理には、opamを使用します。OCaml公式サイトでは、opamで開発環境を管理し、DuneなどのOCaml Platformツールを導入する方法が案内されています。

opamをインストールしたら、初期設定を行います。

Shell
opam init
eval $(opam env)

続いて、このプロジェクト専用の環境を作ります。

Shell
mkdir ocaml-react-app
cd ocaml-react-app

opam switch create . 5.3.0
eval $(opam env)

特定バージョンを指定せず、利用可能なコンパイラを選択しても構いません。

次に、DuneとDreamをインストールします。

Shell
opam install dune dream

Duneは、OCamlの実行ファイル、ライブラリ、テストなどをビルドするためのツールです。

OCamlバックエンドを作る

バックエンド用のディレクトリを作ります。

Shell
mkdir backend
cd backend

dune-projectを作成する

backend/dune-projectを作成します。

backend/dune-project
(lang dune 3.16)

(name ocaml_react_backend)

lang duneには、インストールされているDuneで利用できるバージョンを指定してください。バージョンが分からない場合は、次のコマンドで確認できます。

Shell
dune --version

duneファイルを作成する

続いて、backend/duneを作成します。

backend/dune
(executable
 (name server)
 (libraries dream))

この設定は、server.mlを実行ファイルとしてビルドし、Dreamライブラリを利用することを表しています。

APIを実装する

backend/server.mlを作成します。

backend/server.ml
let hello_handler _request =
  Dream.json
    {|{"message":"Hello from OCaml!"}|}

let health_handler _request =
  Dream.json
    {|{"status":"ok"}|}

let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router
       [
         Dream.get "/api/hello" hello_handler;
         Dream.get "/api/health" health_handler;
       ]

コードの中心となるのは、Dream.routerです。

Dream.get "/api/hello" hello_handler

これは、GET /api/helloへのリクエストをhello_handlerで処理するという意味です。

Dream.jsonを使うと、レスポンスのContent-TypeをJSONとして返せます。

サーバーを起動してみましょう。

Shell
dune exec ./server.exe

デフォルトでは、次のURLでサーバーが起動します。

http://localhost:8080

別のターミナルでAPIを確認します。

Shell
curl http://localhost:8080/api/hello

次のJSONが返れば成功です。

{"message":"Hello from OCaml!"}

ヘルスチェック用APIも確認できます。

Shell
curl http://localhost:8080/api/health
{"status":"ok"}

Reactプロジェクトを作る

プロジェクトのルートディレクトリに戻ります。

Shell
cd ..

Viteを使ってReactプロジェクトを作成します。

Shell
npm create vite@latest frontend -- --template react

作成したディレクトリへ移動し、依存パッケージをインストールします。

Shell
cd frontend
npm install

開発サーバーを起動します。

Shell
npm run dev

ターミナルに表示されたURLをブラウザで開き、Viteの初期画面が表示されることを確認してください。

ViteからOCaml APIへ接続する

開発中は、ReactとOCamlのサーバーが異なるポートで動作します。

React: http://localhost:5173
OCaml: http://localhost:8080

Reactから直接8080番ポートへアクセスすることもできますが、オリジンが異なるため、CORSの設定が必要になる場合があります。

今回はViteのプロキシ機能を使い、/apiへのリクエストをOCamlサーバーへ転送します。

frontend/vite.config.jsを次のように編集します。

frontend/vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true,
      },
    },
  },
});

この設定により、Reactから次のURLへアクセスすると、

/api/hello

Viteがリクエストを次のURLへ転送します。

http://localhost:8080/api/hello

フロントエンド側でバックエンドのホスト名やポート番号を直接指定する必要がなくなるため、コードも管理しやすくなります。

ReactからAPIを呼び出す

frontend/src/App.jsxを次の内容に書き換えます。

frontend/src/App.jsx
import { useEffect, useState } from "react";
import "./App.css";

function App() {
  const [message, setMessage] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadMessage = async () => {
      try {
        const response = await fetch("/api/hello");

        if (!response.ok) {
          throw new Error(`HTTPエラー: ${response.status}`);
        }

        const data = await response.json();
        setMessage(data.message);
      } catch (err) {
        setError("サーバーからメッセージを取得できませんでした。");
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    loadMessage();
  }, []);

  return (
    <main className="container">
      <h1>OCaml × React</h1>

      {loading && <p>読み込み中です。</p>}
      {error && <p className="error">{error}</p>}
      {message && <p className="message">{message}</p>}
    </main>
  );
}

export default App;

このコンポーネントでは、ページが表示されたときにfetchを実行しています。

const response = await fetch("/api/hello");

Viteのプロキシ設定により、このリクエストはOCamlサーバーへ送られます。

レスポンスをJSONへ変換し、messageをReactのstateへ保存します。

const data = await response.json();
setMessage(data.message);

stateが更新されると、取得したメッセージが画面に表示されます。

画面のスタイルを整える

frontend/src/App.cssを次のように編集します。

frontend/src/App.css
:root {
  font-family:
    Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
  color: #1f2937;
  background: #f3f4f6;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

#root {
  min-height: 100vh;
}

.container {
  box-sizing: border-box;
  width: min(720px, calc(100% - 32px));
  margin: 0 auto;
  padding: 80px 0;
  text-align: center;
}

h1 {
  margin-bottom: 32px;
  font-size: clamp(2rem, 6vw, 3.5rem);
}

.message {
  padding: 24px;
  border-radius: 12px;
  background: #ffffff;
  box-shadow: 0 8px 24px rgb(0 0 0 / 8%);
  font-size: 1.25rem;
}

.error {
  padding: 16px;
  border-radius: 8px;
  color: #991b1b;
  background: #fee2e2;
}

Viteのテンプレートに含まれるsrc/index.cssのスタイルが干渉する場合は、内容を削除するか、main.jsxから読み込みを外します。

frontend/src/main.jsxは次のようにします。

frontend/src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

バックエンドとフロントエンドを起動する

開発中は、ターミナルを2つ使用します。

1つ目のターミナルでOCamlサーバーを起動します。

Shell
cd backend
eval $(opam env)
dune exec ./server.exe

2つ目のターミナルでReactを起動します。

Shell
cd frontend
npm run dev

ブラウザでViteのURLを開き、次のメッセージが表示されれば完成です。

Hello from OCaml!

URLパラメーターを受け取る

固定されたメッセージだけでなく、URLから値を受け取るAPIも作れます。

server.mlへ次のハンドラーを追加します。

server.ml
let greet_handler request =
  let name = Dream.param request "name" in
  let escaped_name = Dream.html_escape name in

  Dream.json
    (Printf.sprintf
       {|{"message":"Hello, %s!"}|}
       escaped_name)

ルーターにもエンドポイントを追加します。

Dream.get "/api/greet/:name" greet_handler

全体は次のようになります。

let hello_handler _request =
  Dream.json
    {|{"message":"Hello from OCaml!"}|}

let health_handler _request =
  Dream.json
    {|{"status":"ok"}|}

let greet_handler request =
  let name = Dream.param request "name" in
  let escaped_name = Dream.html_escape name in

  Dream.json
    (Printf.sprintf
       {|{"message":"Hello, %s!"}|}
       escaped_name)

let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router
       [
         Dream.get "/api/hello" hello_handler;
         Dream.get "/api/health" health_handler;
         Dream.get "/api/greet/:name" greet_handler;
       ]

次のURLへアクセスして確認します。

http://localhost:8080/api/greet/OCaml

レスポンスは次のようになります。

{
  "message": "Hello, OCaml!"
}

実際のアプリケーションでJSONを組み立てる場合は、文字列を直接連結するのではなく、JSONライブラリを利用したほうが安全です。データベースやユーザー入力を扱う段階では、Yojsonなどの導入を検討してください。

本番環境ではどう構成するか

開発中は、ViteとDreamを別々に起動しました。本番環境では、主に次の2つの構成が考えられます。

ReactとOCamlを別々に配信する

Reactの静的ファイルをCDNやホスティングサービスから配信し、DreamをAPIサーバーとして公開する方法です。

ブラウザ
├── Reactの静的ファイル
└── OCaml API

フロントエンドとバックエンドを独立してデプロイできるため、大規模な構成にも対応しやすくなります。

DreamからReactのビルド結果を配信する

Reactをビルドします。

Shell
cd frontend
npm run build

frontend/distにHTML、JavaScript、CSSなどの静的ファイルが生成されます。このファイルをDreamのDream.staticで配信すれば、1つのOCamlサーバーからAPIとReactの両方を提供できます。

本番環境では、存在しないフロントエンドのURLをindex.htmlへ戻す処理、キャッシュ設定、圧縮、TLS、セキュリティヘッダーなども必要です。

まずはAPIと画面を別々に開発し、アプリケーションの規模に合わせて配信方法を選ぶとよいでしょう。

おわりに

この記事では、OCamlのDreamでWeb APIを作り、Reactから呼び出すところまでを実装しました。

OCamlは、静的型付けとパターンマッチを活用しながら、堅牢なサーバー処理を記述できる言語です。一方、Reactはコンポーネント単位でインタラクティブな画面を構築できます。

両者を組み合わせることで、次のように役割を分担できます。

OCaml
└── API、認証、データ処理、ビジネスロジック

React
└── 画面表示、フォーム、ユーザー操作、状態管理

今回はメッセージを返すだけの小さなAPIでしたが、同じ構成を使って、データベースへの接続、ログイン機能、投稿の作成・更新・削除などへ発展させられます。

OCamlのWeb開発を試してみたい場合は、まずDreamで小さなJSON APIを作り、Reactと接続するところから始めてみましょう。