Expressで実験データサーバーを作る
これまでfetchでサーバーにデータをリクエストする方法を学びました。今回はその反対側 — データを送り返すサーバーを自分で作ります。
実験室でLIMS(Laboratory Information Management System)を使うように、Webアプリケーションにもデータを保存し取り出すサーバーが必要です。Expressはそのサーバーを作るツールです。前回学んだNode.jsの上で動くWebフレームワーク — 簡単に言えば、サーバー開発用の実験キットです。
Node.jsだけでもサーバーは作れますが(http.createServerを覚えていますか?)、すべて自前で実装しなければなりません。Expressを使えば、URLに応じて異なるデータを送る、ファイルを提供する、エラーを処理するといった繰り返し作業をフレームワークが代行します。試薬を一つずつ購入して自分で調合する代わりに、検証済みキットを使うのと同じです。
Hello World:3行でサーバーを始める
Express サーバーの最も基本的な形です:
const express = require("express");
const app = express();
app.get("/", function(req, res) {
res.send("LIMS Server Running");
});
app.listen(3000, function() {
console.log("サーバー起動: http://localhost:3000");
});このコードをserver.jsとして保存し、ターミナルでnode server.jsを実行すると、ブラウザでhttp://localhost:3000にアクセスできます。
コア要素を分解すると:
require("express")— Expressライブラリを読み込むapp.get("/", ...)— "/" アドレスにアクセスしたら実行する関数を登録req— リクエスト(request)。クライアントが送った情報res— レスポンス(response)。サーバーが送り返す情報res.send()— クライアントにデータを送るapp.listen(3000)— ポート3000でリクエストを待機
ルーティング:URLに応じて異なるデータを送る
ルーティング(routing)は「どのURLにリクエストが来たら、どのデータを返すか」を決めることです。LIMSで試料番号を入力するとその試料情報が出て、プロトコル名を入力するとそのプロトコルが出るのと同じです。
const express = require("express");
const app = express();
app.get("/", function(req, res) {
res.send("実験データサーバーへようこそ");
});
app.get("/genes", function(req, res) {
const genes = [
{ name: "TP53", chromosome: "17p13.1", type: "tumor suppressor" },
{ name: "BRCA1", chromosome: "17q21.31", type: "DNA repair" },
{ name: "EGFR", chromosome: "7p11.2", type: "receptor tyrosine kinase" }
];
res.json(genes);
});
app.get("/protocols", function(req, res) {
const protocols = ["DNA Extraction", "PCR", "Western Blot", "ELISA"];
res.json(protocols);
});
app.listen(3000, function() {
console.log("サーバー起動: http://localhost:3000");
});http://localhost:3000/genesにアクセスすると遺伝子リストが、/protocolsにアクセスするとプロトコルリストがJSONで表示されます。res.json()はJavaScriptオブジェクトをJSON形式に変換して送ります。
動的ルーティング:URLからパラメータを取得
特定の遺伝子情報だけを照会したい場合、URLにパラメータを入れます:
const geneDatabase = {
TP53: { name: "TP53", chromosome: "17p13.1", function: "tumor suppressor", length_bp: 19149 },
BRCA1: { name: "BRCA1", chromosome: "17q21.31", function: "DNA repair", length_bp: 81189 },
EGFR: { name: "EGFR", chromosome: "7p11.2", function: "receptor tyrosine kinase", length_bp: 188307 }
};
app.get("/gene/:id", function(req, res) {
const geneId = req.params.id;
const gene = geneDatabase[geneId];
if (gene) {
res.json(gene);
} else {
res.status(404).json({ error: "該当する遺伝子が見つかりません", query: geneId });
}
});/gene/TP53にアクセスするとTP53の情報が、/gene/XYZ123にアクセスすると404エラーが返されます。:idの部分がURLパラメータ — リクエストごとに異なる値が入り、req.params.idで取り出して使います。
ミドルウェア:実験の前処理ステップ
ミドルウェア(middleware)はExpressの核心概念です。リクエストがサーバーに到着してからレスポンスが送出されるまでの間に通る処理ステップです。
実験で試料が分析装置に入る前に通る前処理過程を考えてみてください:
[試料受付] → [ラベル確認] → [QC検査] → [前処理] → [分析] → [結果報告]Expressでもリクエストが同様のパイプラインを通ります:
[リクエスト到着] → [ミドルウェア1] → [ミドルウェア2] → [ルートハンドラ] → [レスポンス送信]ミドルウェアはapp.use()で登録します:
const express = require("express");
const app = express();
app.use(function(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
app.get("/genes", function(req, res) {
res.json([{ name: "TP53" }, { name: "BRCA1" }]);
});
app.listen(3000);app.use()で登録した関数はすべてのリクエストに対して実行されます。ここではリクエストが来るたびに時刻、メソッド(GET/POST)、URLをコンソールに記録します。実験ノートに「何時に、どの試料で、どの分析をしたか」を記録するのと同じです。
重要なのはnext()です。この関数を呼ばないと次のステップ(次のミドルウェアまたはルートハンドラ)に進みません。next()を呼ばなければリクエストはここで止まりレスポンスが送られません — QCで不合格になった試料が次のステップに進めないのと同じです。
実践ミドルウェア:POSTデータのパース
クライアントがPOST方式でデータを送る場合(例:新しい試料の登録)、そのデータを読めるようにパースする必要があります。Expressはそのための組み込みミドルウェアを提供します:
app.use(express.json());
app.post("/samples", function(req, res) {
const newSample = req.body;
console.log("新規試料登録:", newSample);
res.json({ message: "試料登録完了", sample: newSample });
});express.json()がなければreq.bodyはundefinedになります。このミドルウェアがJSON形式で送られてきたデータを自動でパースしreq.bodyに格納します。
総合例:試料管理API
これまで学んだことを合わせて、簡単な試料管理サーバーを作りましょう:
const express = require("express");
const app = express();
app.use(express.json());
app.use(function(req, res, next) {
console.log(`[LOG] ${req.method} ${req.url}`);
next();
});
const samples = [
{ id: "S001", name: "Blood Sample A", od: 1.85, status: "pass" },
{ id: "S002", name: "Tissue Sample B", od: 0.42, status: "fail" },
{ id: "S003", name: "Serum Sample C", od: 2.10, status: "pass" }
];
app.get("/samples", function(req, res) {
res.json(samples);
});
app.get("/sample/:id", function(req, res) {
const found = samples.find(function(s) {
return s.id === req.params.id;
});
if (found) {
res.json(found);
} else {
res.status(404).json({ error: "試料が見つかりません" });
}
});
app.get("/samples/passed", function(req, res) {
const passed = samples.filter(function(s) {
return s.status === "pass";
});
res.json({ count: passed.length, samples: passed });
});
app.listen(3000, function() {
console.log("試料管理サーバー起動: http://localhost:3000");
});このサーバーは3つのAPIエンドポイントを提供します:
GET /samples— 全試料リストGET /sample/S001— 特定の試料を照会GET /samples/passed— QC合格試料のみフィルタリング
前回学んだfetchでこのサーバーにリクエストすると:
fetch("http://localhost:3000/samples/passed")
.then(function(response) { return response.json(); })
.then(function(data) { console.log(data); });
// { count: 2, samples: [{ id: "S001", ... }, { id: "S003", ... }] }フロントエンド(fetch)とバックエンド(Express)が繋がる瞬間です。NCBIで遺伝子を検索する時に裏側で起きていることのミニチュア版です。
やってみよう(Faded Example)
空欄を埋めて遺伝子情報を返すExpressルートを完成させてください。
const express = require("express");const app = express();app.get("/gene/", function(req, res) {const geneId = req..id;res.json({ gene: geneId, found: true });});app.listen();
よくあるエラーと解決法
Q: Cannot GET /パスエラーが出ます
そのパスに対するルートが登録されていません。app.get("/パス", ...)のコードがあるか、タイプミスがないか確認してください。Expressは登録されたルートがなければ自動でCannot GETメッセージを返します。
Q: req.bodyがundefinedです
app.use(express.json())がルートより上に宣言されているか確認してください。ミドルウェアはコードの順序どおりに実行されます。JSONパースミドルウェアがルートの後にあると、リクエストがルートに到達した時点でまだパースされていない状態です。
Q: サーバーを修正したのに反映されません
Node.jsはコードを修正しても自動では反映されません。ターミナルでCtrl+Cでサーバーを停止し、再度node server.jsで起動してください。自動再起動が必要ならnodemonをインストールしてnodemon server.jsで実行します。
Q: Error: listen EADDRINUSE: address already in use :::3000
すでにポート3000を使用しているプログラムがあります。以前起動したサーバーがまだ動いている可能性があります。lsof -i :3000で確認して終了するか、ポート番号を3001などに変更してください。
Q: Expressは「フレームワーク」と言いますが、npmでインストールする他のパッケージ(mysql2など)と何が違いますか?
核心的な違いは誰が誰を呼ぶかです。mysql2はライブラリ — 自分が必要な時にconnection.query()を呼びます。Expressはフレームワーク — app.get("/path", コールバック)のように自分のコードを登録すると、リクエストが来た時にExpressが自分のコールバックを呼びます。
この概念を「制御の反転(Inversion of Control)」と言います。より詳しい例えと説明はフレームワークとライブラリトピックで扱います。