TypeScriptでExpressのミドルウェアを型安全に定義する方法!バックエンド開発の初心者向け解説
生徒
「TypeScriptとExpressを使ってウェブサーバーのバックエンド開発に挑戦しているのですが、ミドルウェアという仕組みを型安全に定義する方法がよく分かりません。」
先生
「TypeScriptとExpressの組み合わせはバックエンド開発で非常に人気があります。ミドルウェアを型安全に定義することで、データの受け渡しやエラーの発生を防ぐことができるようになります。」
生徒
「型安全に定義するというのは、具体的にどのようにプログラムを書けば良いのでしょうか。パソコンの操作やプログラミングの経験があまりないので、初歩的なところから教えてほしいです。」
先生
「型安全の基礎からミドルウェアの具体的な書き方まで、初心者の方にも分かりやすいように順番に解説していきます。それでは、基本的な使い方を見ていきましょう!」
1. バックエンド開発とExpressの基礎知識
ウェブサイトやスマートフォンのアプリが動くとき、画面の裏側でデータを処理する仕組みをバックエンド開発やサーバーサイド開発と呼びます。私たちが普段見ている画面の裏では、データベースから情報を探したり、ユーザーのログイン状態を確認したりするプログラムが動いています。この仕組みを作るために使われるのがNode.jsという環境です。
そして、Node.jsの環境でウェブサーバーを簡単に作るための道具箱のようなものがExpressという仕組みです。Expressを使うと、特定のURLにアクセスがあったときにどのような返事を返すかという処理を、短いプログラムで記述することができます。しかし、JavaScriptだけでExpressのプログラムを書くと、文字の間違いやデータの種類の勘違いによって、本番のシステムでエラーが発生してしまう原因になります。
2. TypeScriptを導入する理由と型安全の意味
そこで登場するのがTypeScriptです。TypeScriptは、通常のJavaScriptに型というルールを追加したプログラミング言語です。ここで言う「型」とは、プログラムの中で扱うデータが「文字」なのか「数字」なのか「日付」なのかという種類を、あらかじめ明確に指定しておく約束事のことを指します。
プログラミングにおいて型安全とは、このデータの種類に関する間違いを、プログラムを実行する前の段階でパソコンが自動的に見つけて教えてくれる状態のことを言います。型安全が保証されていると、存在しない命令を呼び出そうとしたり、数字を入れるべき場所に文字を入れてしまったりするミスを完全に防ぐことができます。これにより、開発の途中でバグを発見しやすくなり、結果として安全で安定したバックエンドのシステムを作ることができるようになります。
3. ミドルウェアとは何かを簡単な例えで理解する
Expressにおけるミドルウェアとは、ウェブサーバーがユーザーからのリクエストを受け取ってから、最終的な返事(レスポンス)を画面に返すまでの間に、途中で割り込んで特定の処理を行う関数(プログラムのまとまり)のことです。文字通り、中間(ミドル)で働くソフトウェアという意味を持っています。
これを日常生活で例えるなら、テーマパークの入り口にある「手荷物検査」や「チケット確認の窓口」のようなものです。アトラクションという最終目的地にたどり着く前に、怪しい荷物を持っていないか、有効なチケットを持っているかを順番に確認する係員がミドルウェアに該当します。もしチケットを持っていなければ、その時点でアトラクションには進めず、お帰りいただくことになります。このように、すべてのアクションに共通する共通処理を切り離して管理する仕組みがミドルウェアです。
4. 最もシンプルなミドルウェアの基本構造
Expressのミドルウェアは、基本的に3つの情報を受け取って動きます。それは、ユーザーからの要求情報が入ったRequest、ユーザーへの返事を作成するResponse、そして次の処理に進むための合図であるnextという命令です。この3つを順番に処理していくのがミドルウェアのルールです。
まずは、どのようなアクセスがあったかを黒い画面に記録するだけの、最も単純なログ出力ミドルウェアのプログラムを見てみましょう。TypeScriptで記述することで、それぞれの情報がどのような型を持っているかを明示することができます。
import express, { Request, Response, NextFunction } from 'express';
const app = express();
const simpleLogger = (req: Request, res: Response, next: NextFunction) => {
console.log("アクセスを検知しました");
next();
};
app.use(simpleLogger);
上記のプログラムでは、Expressが提供している「Request」「Response」「NextFunction」という型を、コロンの後に指定しています。これにより、変数の中身が正しく定義されていることが保証されます。処理の最後にある「next()」を実行することで、手荷物検査が終了し、次の処理へ進むことができます。これを忘れると、処理が途中で止まってしまい、画面がいつまでも読み込み中のままになってしまうので注意が必要です。
5. 特定の条件でアクセスを拒否するミドルウェア
次に、特定の条件を満たしていない場合に、次の処理に進ませずにその場でエラーを返すミドルウェアのプログラムを作成してみましょう。ここでは、URLの末尾に特定の秘密の言葉(キーワード)が含まれているかどうかをチェックする簡単な仕組みを考えます。
プログラミングにおける条件分岐を使い、合言葉が一致していれば次の処理へ進み、一致していなければアクセスを拒否するという制御を行います。このとき、型を指定しているおかげで、リクエストの中身を安全に調べることができます。
import { Request, Response, NextFunction } from 'express';
const secretCheckMiddleware = (req: Request, res: Response, next: NextFunction) => {
const password = req.query.pass;
if (password === 'open-sesame') {
next();
} else {
res.status(403).send('お探しのページにアクセスする権限がありません。');
}
};
このプログラムでは、「req.query.pass」という部分で、URLに含まれる文字を読み取っています。もし文字が「open-sesame」であれば、無事に「next()」が呼び出されて先へ進めます。そうでない場合は、ステータスコードと呼ばれる数字の「403(閲覧禁止という意味の番号)」を設定し、画面に警告のメッセージを表示して処理をその場で終了します。
6. リクエストオブジェクトに新しい型を拡張する方法
バックエンド開発を進めていくと、ミドルウェアの途中でユーザーの情報を調べて、その調べた結果のデータを後ろの処理に引き継ぎたいケースがよくあります。例えば、ログインしているユーザーの番号をミドルウェアで確認し、その番号をメインの処理で使いたい場合などです。
しかし、TypeScriptの標準の「Request」という型には、私たちが独自に追加したい変数(例えばユーザー情報など)の名前が最初から登録されていません。そのため、そのまま新しいデータを追加しようとすると、パソコンが「そのようなデータは登録されていません」とエラーを出してしまいます。この問題を解決するために、既存の型に新しい項目の定義を合体させる型拡張という設定を行います。
import { Request, Response, NextFunction } from 'express';
interface CustomUserRequest extends Request {
userId?: string;
}
const userAuthMiddleware = (req: CustomUserRequest, res: Response, next: NextFunction) => {
req.userId = "user_9999";
next();
};
上記のコードでは、「interface」という構文を使って、Express本来の「Request」の機能を引き継ぎつつ、新しく「userId」という文字列のデータを入れることができる専用の型「CustomUserRequest」を独自に作成しています。これにより、ミドルウェアの中でリクエストにデータを安全に詰め込むことができるようになり、後ろの処理でもそのデータを型安全に活用することができます。
7. エラーハンドリング専用のミドルウェアを型安全に定義する
プログラムの実行中に予想外のトラブルが発生したとき、システム全体が突然停止してしまうのを防ぐために、発生したエラーを専門に処理するミドルウェアを用意することがあります。これをエラーハンドリングミドルウェアと呼びます。
通常のミドルウェアは3つの情報(引数)を受け取りますが、エラーハンドリング用のミドルウェアは、最初にエラーの内容を受け取るため、合計で4つの情報を受け取るという特別なルールがあります。TypeScriptでは、この一番目の情報にも適切な型を指定することで、エラーの内容に応じた親切なメッセージを画面に返す処理を安全に記述することができます。
import { Request, Response, NextFunction } from 'express';
const globalErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
console.error("システム内部で問題が発生しました:", err.message);
res.status(500).send("申し訳ありません。サーバー側でエラーが発生しました。");
};
このプログラムでは、「err」という変数に発生した問題の詳細が届きます。重大な問題が発生したことを意味する「500」というステータスコードをユーザーに返しつつ、裏側のログ画面に詳しいエラー原因を書き出しています。これにより、利用者に不親切なエラー画面を見せることなく、開発者が後から問題を調査しやすくなります。
8. 型安全なミドルウェアを実際のアプリに組み込む手順
これまで作成してきた型安全なミドルウェアを、実際に動くExpressのアプリケーション全体の中に組み込んで動かすための全体像を確認しましょう。複数のミドルウェアを組み合わせることで、強固なバックエンドの壁を作ることができます。
プログラムは上から順番に実行されるため、ミドルウェアを登録する順番が非常に重要になります。すべてのアクセスに対して一律に適用したいログ出力などは、プログラムの上のほうに記述するのが一般的なルールです。
import express from 'express';
const app = express();
app.use((req, res, next) => {
console.log("全体ミドルウェアを通過しました");
next();
});
app.get('/dashboard', (req, res) => {
res.send("ダッシュボードのメイン画面です。");
});
app.listen(3000, () => {
console.log("サーバーがポート3000で起動しました");
});
このプログラムを実行すると、パソコンの中に仮想的なサーバーが立ち上がり、アクセスを待ち受ける状態になります。実際にブラウザなどからアクセスがあると、まず登録されたミドルウェアが動き、その後にメインの画面の文字が表示されます。黒い画面には、処理が通過した順番にログのメッセージが出力されます。
9. 動作確認の方法と出力結果の味方
プログラムが完成したら、意図した通りにミドルウェアが型安全に動いているかを実際に動かして確認します。エラーが発生せずに、順番通りに処理が進んでいるかをコンソールと呼ばれる出力画面で観察します。
正しく設定されたミドルウェアを経由してサーバーが起動し、ユーザーからの要求を処理した際、画面の裏側では次のような文字が出力されます。これを確認することで、途中の手荷物検査が正しく機能しているかを判断することができます。
サーバーがポート3000で起動しました
全体ミドルウェアを通過しました
アクセスを検知しました
もし型に合わないおかしなプログラムを書いてしまっていた場合は、この起動の処理を行う前の段階で、パソコンの画面に真っ赤な文字でエラー原因が表示され、不完全なプログラムが動いてしまうのを防いでくれます。これこそが、TypeScriptを導入して型安全にミドルウェアを定義する最大のメリットです。