はじめに
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/helloOCamlサーバーは、次のような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をインストールしたら、初期設定を行います。
opam init
eval $(opam env)続いて、このプロジェクト専用の環境を作ります。
mkdir ocaml-react-app
cd ocaml-react-app
opam switch create . 5.3.0
eval $(opam env)特定バージョンを指定せず、利用可能なコンパイラを選択しても構いません。
次に、DuneとDreamをインストールします。
opam install dune dreamDuneは、OCamlの実行ファイル、ライブラリ、テストなどをビルドするためのツールです。
OCamlバックエンドを作る
バックエンド用のディレクトリを作ります。
mkdir backend
cd backenddune-projectを作成する
backend/dune-projectを作成します。
(lang dune 3.16)
(name ocaml_react_backend)lang duneには、インストールされているDuneで利用できるバージョンを指定してください。バージョンが分からない場合は、次のコマンドで確認できます。
dune --versionduneファイルを作成する
続いて、backend/duneを作成します。
(executable
(name server)
(libraries dream))この設定は、server.mlを実行ファイルとしてビルドし、Dreamライブラリを利用することを表しています。
APIを実装する
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として返せます。
サーバーを起動してみましょう。
dune exec ./server.exeデフォルトでは、次のURLでサーバーが起動します。
http://localhost:8080別のターミナルでAPIを確認します。
curl http://localhost:8080/api/hello次のJSONが返れば成功です。
{"message":"Hello from OCaml!"}ヘルスチェック用APIも確認できます。
curl http://localhost:8080/api/health{"status":"ok"}Reactプロジェクトを作る
プロジェクトのルートディレクトリに戻ります。
cd ..Viteを使ってReactプロジェクトを作成します。
npm create vite@latest frontend -- --template react作成したディレクトリへ移動し、依存パッケージをインストールします。
cd frontend
npm install開発サーバーを起動します。
npm run devターミナルに表示されたURLをブラウザで開き、Viteの初期画面が表示されることを確認してください。
ViteからOCaml APIへ接続する
開発中は、ReactとOCamlのサーバーが異なるポートで動作します。
React: http://localhost:5173
OCaml: http://localhost:8080Reactから直接8080番ポートへアクセスすることもできますが、オリジンが異なるため、CORSの設定が必要になる場合があります。
今回はViteのプロキシ機能を使い、/apiへのリクエストをOCamlサーバーへ転送します。
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/helloViteがリクエストを次のURLへ転送します。
http://localhost:8080/api/helloフロントエンド側でバックエンドのホスト名やポート番号を直接指定する必要がなくなるため、コードも管理しやすくなります。
ReactからAPIを呼び出す
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を次のように編集します。
: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は次のようにします。
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サーバーを起動します。
cd backend
eval $(opam env)
dune exec ./server.exe2つ目のターミナルでReactを起動します。
cd frontend
npm run devブラウザでViteのURLを開き、次のメッセージが表示されれば完成です。
Hello from OCaml!URLパラメーターを受け取る
固定されたメッセージだけでなく、URLから値を受け取るAPIも作れます。
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をビルドします。
cd frontend
npm run buildfrontend/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と接続するところから始めてみましょう。
