【Node.js実戦習得】第8回:Expressフレームワーク入門!ECカートAPIの移行と型安全ルーティング

社員ブログ

1. Expressの正体と「ミドルウェア思想」の核心

酒本先輩!

第7回は中の処理が面倒でした……。

タスクやカートのデータをPOSTするだけで req.on('data') とか req.on('end') を書かされるなんて、Node.jsってなんて原始的なんだって絶望しかけましたよ

熊木くん。

でも、あの『生のネットワーク処理』を確認したからこそ、今日から触る Express の強力さが心の底から理解できるはずよ。
まず質問だけど、Expressって、Node.jsとは別の新しい実行エンジンだと勘違いしていない?

えっ、違うんですか?

Expressっていうサーバーを新しく立ち上げるイメージでした

ブブー、大間違い!

Expressの正体は、前回私たちが書いた node:http モジュールを裏側でそのまま叩いている、ただの『薄いラッパーライブラリ』よ。
Expressがやってくれているのは、主に次の2つだけ。

  1. IncomingMessageServerResponse を使いやすく拡張し、便利なメソッド(.json().status())を生やすこと。
  2. 複雑な条件分岐(if文の嵐)をなくし、関数を数珠つなぎにして処理する『ミドルウェア(Middleware)アーキテクチャ』を提供すること。

特にこのミドルウェア思想がバックエンド開発では超重要よ。リクエストが来てからレスポンスを返すまでの間に、データ解析、認証チェック、ロギング、バリデーションといった処理を『パイプライン(数珠つなぎ)』のように挟み込んでいく設計のことね。
言葉で聞くよりコードを見た方が早いわ。

さっそく、私たちのプロジェクトにExpressを迎え入れましょう!

2. 【ハンズオン①】Expressの導入とシンプルなサーバー起動

まずは、前回の node-ts-core プロジェクトにExpress本体と、TypeScriptで開発するために絶対不可欠な型定義ファイルをインストールするよ!

# Express本体のインストール
npm install express

# TypeScript用の型定義ファイルを開発依存(-D)としてインストール
npm install -D @types/express

インストール完了しました! package.jsondependenciesexpress が追加されたのを確認しました

OK。

まずはExpressがどれだけ直感的にサーバーを起動できるか、src/index.ts を完全にクリアにして、シンプルなExpressサーバーのコードを書いてみて!

// src/index.ts
import express, { Request, Response } from 'express';

// 1. Expressアプリケーションのインスタンスを生成
const app = express();
const PORT = 3000;

// 2. ルーティングの定義(GETメソッドで、ルートパス「/」にアクセスされた場合)
app.get('/', (req: Request, res: Response) => {
  // 標準モジュールの writeHead や end を使わず、1行でステータスとテキストを返せる!
  res.status(200).send('Hello, Expressのモダンな世界へようこそ!');
});

// 3. 指定したポートで接続を待ち受ける
app.listen(PORT, () => {
  console.log(`Expressサーバーが http://localhost:${PORT} で起動しました!`);
});

これだけですか!?

前回の標準モジュールに比べて、圧倒的にスッキリしていますね。

npm run dev で動かしてみます!

npm run dev

実行結果(ターミナル)

Expressサーバーが http://localhost:3000 で起動しました!

ブラウザで http://localhost:3000/ にアクセスしたら、一瞬でテキストが表示されました!

ルーティングが app.get('/', ...) というメソッドチェーンの形で書けるのが、直感的でめちゃくちゃ簡単です!

でしょ?

if文で url === '/' && method === 'GET' って泥臭く書く必要が一切なくなるの。

メソッドごとに最初から関数が分かれているのがExpressの基本スタイルよ

3. 【ハンズオン②】ECカートAPIを完全移行

さあ、ここからが本番よ。

第7回で書いた『ECカートAPI』を、Expressを使って完全に近代化リファクタリング(移行)するよ。
あの面倒な処理 req.on('data') を、Expressの機能で綺麗さっぱり消し去りましょう!

// src/index.ts
import express, { Request, Response } from 'express';

const app = express();
const PORT = 3000;

// カート内商品の型定義
interface CartItem {
  id: number;
  productName: string;
  price: number;
  quantity: number;
}

// 疑似データベース(メモリ上に保持する配列)
let cartItems: CartItem[] = [
  { id: 1, productName: 'プログラミング用高級キーボード', price: 35000, quantity: 1 },
  { id: 2, productName: 'Type-C 急速充電ケーブル 1.5m', price: 1800, quantity: 2 }
];

// 【超重要】Express内蔵の組み込みミドルウェアを有効化
// これを書くだけで、リクエストのストリームを裏で自動解析し、req.body にJSONを突っ込んでくれる!
app.use(express.json());


// 1. 【GET /api/cart】 カート内商品の一覧取得
app.get('/api/cart', (req: Request, res: Response) => {
  // .json() メソッドを呼ぶだけで、Content-Type: application/json や utf-8 のヘッダーをExpressが自動付与!
  res.status(200).json(cartItems);
});


// 2. 【POST /api/cart】 カートへの商品追加
app.post('/api/cart', (req: Request, res: Response) => {
  // ストリームのイベント監視は一切不要!すでにオブジェクトとしてパースされている
  const { productName, price, quantity } = req.body;

  // バリデーション(データチェック)
  if (!productName || typeof price !== 'number' || typeof quantity !== 'number') {
    return res.status(400).json({ message: '不正なリクエストデータです。' });
  }

  if (quantity <= 0 || price < 0) {
    return res.status(400).json({ message: '価格は0以上、数量は1以上である必要があります。' });
  }

  const newItem: CartItem = {
    id: cartItems.length > 0 ? Math.max(...cartItems.map(item => item.id)) + 1 : 1,
    productName,
    price,
    quantity
  };

  cartItems.push(newItem);

  // 201 Created で返却
  res.status(201).json(newItem);
});


// サーバーの起動
app.listen(PORT, () => {
  console.log(`カート管理Expressサーバーが起動: http://localhost:${PORT}`);
});

先輩……!

コードの美しさが次元違いです!
あの、データの断片を文字列としてちまちま結合していた req.on('data') の記述が完全に絶滅しています。

これ、裏では何が起きているんですか?

それが、上の方に書いた app.use(express.json()) という一行の素晴らしさよ!
これがExpressの誇る『組み込みミドルウェア』。

リクエストがルーティング(app.post)に到達する手前で、送られてきたバイナリデータのストリームを裏側で全部キャッチして結合し、JSON.parse まで済ませてから、次の関数にバトンタッチしてくれるの。
だから、私たちは何事もなかったかのように req.body から綺麗なオブジェクトを取り出すことができるのよ!

4. フロントエンド開発者が泣いて喜ぶ「URLパラメータ」の超簡単パース

Express、凄すぎます……。

そういえば前回、先輩が『標準モジュールだとURLパラメータの解析(/api/cart/2 から 2 を抜く処理)が限界がある』って言っていましたよね。

Expressならどう書くんですか?

よくぞ聞いてくれたわ。

Expressお得意の、コロン(:)を使ったURLパラメータ機能の出番よ!
カートから特定の商品を削除する DELETE 処理と、数量を変更する PUT 処理を新しく追加してみましょう!

/* 先ほどのコードの app.post の下に追加 */

// 3. 【PUT /api/cart/:id】 カート内商品の数量変更(更新)
app.put('/api/cart/:id', (req: Request, res: Response) => {
  // URLの「:id」の部分は、自動的に req.params.id に文字列として格納される!文字列だから10進数にパースするわよ
  const itemId = parseInt(req.params.id, 10);
  const { quantity } = req.body;

  if (isNaN(itemId) || typeof quantity !== 'number' || quantity <= 0) {
    return res.status(400).json({ message: '不正なリクエストデータです。' });
  }

  // 対象の商品を検索
  const item = cartItems.find(i => i.id === itemId);
  if (!item) {
    return res.status(404).json({ message: '指定された商品がカート内に見つかりません。' });
  }

  // 数量を更新
  item.quantity = quantity;
  res.status(200).json(item);
});


// 4. 【DELETE /api/cart/:id】 カート内商品の削除
app.delete('/api/cart/:id', (req: Request, res: Response) => {
  const itemId = parseInt(req.params.id, 10);

  if (isNaN(itemId)) {
    return res.status(400).json({ message: '無効なID形式です。' });
  }

  const itemIndex = cartItems.findIndex(i => i.id === itemId);
  if (itemIndex === -1) {
    return res.status(404).json({ message: '指定された商品が見つかりません。' });
  }

  // 配列から該当商品を削除
  const deletedItem = cartItems.splice(itemIndex, 1);
  res.status(200).json({ message: 'カートから商品を削除しました', data: deletedItem[0] });
});

おおお!

URLに /api/cart/:id と書くだけで、自動的に req.params.id から値が取れるなんて便利すぎます!

フロントエンドで react-router を使っていた時の感覚のままバックエンドが書けます!

そう、ExpressがURLを内部で自動解析して、連想配列オブジェクトにマッピングしてくれているの。

これがあるから、実務の複雑なREST API(例:/api/users/:userId/orders/:orderId)も、迷子にならずに綺麗に構築できるのよ

5. 【TypeScript深化】ジェネリクスによる完璧な「型安全リクエスト」の設計

さあ、ここから更にパワーアップするわよ。

熊木くん、今のままでも動くけれど、エディタ上で req.bodyreq.params にマウスカーソルを当ててみて。

型がどうなっている?

あ……

req.body の型が any になっています!

ということは、せっかくTypeScriptを使っているのに、存在しないプロパティ(req.body.hoge)を呼び出してもエラーにならず、実行時までバグに気づけないってことですか?

その通り!

Expressは素のままだとJavaScript時代の名残で、リクエストの中身を any として扱っちゃうの。

バックエンドの any は死を意味するわ。
そこで、Expressの Request 型が持っているジェネリクス(型の引数)を使って、リクエストの型定義をガチガチに縛り付けましょう!

Expressの Request インタフェースの定義は、実は以下のように4つの型引数を受け取れる構造になっているの。

 

$$\text{Request}<\text{Params}, \text{ResBody}, \text{ReqBody}, \text{ReqQuery}>$$

  • 第1引数 (Params): req.params の型(URLパラメータ)
  • 第2引数 (ResBody): res.json() で返すレスポンスボディの型
  • 第3引数 (ReqBody): req.body の型(送られてくるJSONの中身)
  • 第4引数 (ReqQuery): req.query の型(URL末尾の ?search=xxx などのクエリ)

この仕組みを使って、さっきの PUT(数量変更)のルーティングを、型安全仕様にアップグレードしてみて!

/* PUTルーティングの部分を以下のように型安全に書き換え */

// 1. URLパラメータの型を定義
interface UpdateParams {
  id: string; // ※req.paramsの中身は最初すべて文字列で入ってくるわ
}

// 2. 送られてくるリクエストボディの型を定義
interface UpdateRequestBody {
  quantity: number;
}

// 3. Request型にジェネリクスを適用(レスポンス型は使わないので空のオブジェクト {} を挟む)
app.put('/api/cart/:id', (req: Request<UpdateParams, {}, UpdateRequestBody>, res: Response) => {
  const itemId = parseInt(req.params.id, 10);

  // TypeScriptの型チェックが炸裂!
  // もしここで「req.body.quantyty」(タイポ)と書くと、エディタ上で即座に赤線エラーを出してくれる!
  const { quantity } = req.body; 

  if (isNaN(itemId) || typeof quantity !== 'number' || quantity <= 0) {
    return res.status(400).json({ message: '不正なリクエストデータです。' });
  }

  const item = cartItems.find(i => i.id === itemId);
  if (!item) {
    return res.status(404).json({ message: '商品が見つかりません。' });
  }

  item.quantity = quantity;
  res.status(200).json(item);
});

す、すごすぎる……!

req.body. と打ち込んだ瞬間に、エディタが quantity を自動補完してくれました!

これならタイポによるバグが100%防げますし、何よりコードのドキュメント性が高まりますね!

これが大規模な実務のバックエンドでTypeScriptが選ばれる最大の理由よ。
仕様変更でリクエストの項目名が変わったときも、この型定義を1箇所変えるだけで、プロジェクト全体の修正漏れがコンパイルエラーとしてあぶり出されるの。

最強の盾を手に入れた気分でしょ?

6. 実務での注意点:フレームワークに溺れるな、低レイヤを忘れるな

酒本先輩、もうExpressが便利すぎて、前回の標準モジュールのことなんて忘れちゃいそうです。

これからはExpressの書き方だけ覚えておけば世の中渡っていけますよね?

それ、典型的なフレームワークしか知らないエンジニアだわ。
最初に言った通り、Expressはあくまで標準モジュールの『薄い皮(ラッパー)』に過ぎないの。

 

例えば、実務で『数ギガバイトある超巨大なCSVファイルをアップロードするAPI』を作ることになったとするわよね。

そのとき、何も考えずに app.use(express.json()) の感覚でメモリに全部データを載せようとしたら、サーバーのメモリが一瞬で枯渇して、Webサーバー全体がクラッシュ(OOM: Out of Memory)してしまうの。
そのとき、どうやって解決する?

えっ……。

あ!前回の『ストリーム(分割受信)』の知識を使って、データを少しずつパイプラインでディスクに流し込むような処理が必要になる……?

その通り!

フレームワークの便利な部分だけに頼っているエンジニアは、そういうトラブルが起きた時に手も足も出なくなるの。
便利なExpressを使いこなしつつも、裏では常に『生のHTTP通信』や『ストリーム』が動いているという低レイヤの視点を忘れないこと。

これが、ただのコード書きから『アーキテクト』へ進化するための境界線よ!

本日のまとめ

  • Expressの正体: Node.js標準の http モジュールを使いやすく抽象化した、不動のシェアNo.1を誇る「薄いWebフレームワーク」。
  • ミドルウェア(Middleware): リクエストを受け取ってからレスポンスを返すまでのパイプライン処理。app.use(express.json()) によって、面倒なストリーム解析を自動化できる。
  • URLパラメータ: app.delete('/api/cart/:id') のようにパスにコロンを打つことで、動的なURLから req.params.id で簡単に値を取得できる。
  • 型安全リクエスト(ジェネリクス): Request<Params, ResBody, ReqBody, ReqQuery> の型引数を指定することで、req.bodyreq.paramsany を排除し、完全な静的型チェックと自動補完の恩恵を受けられる。

次回:第9回「Expressミドルウェアを深堀する!共通ロギング・エラーハンドリング設計」に続く

タイトルとURLをコピーしました