カテゴリ: TypeScript 更新日: 2026/06/24

TypeScriptとExpressで学ぶ!ミドルウェアの依存注入(DI)によるバックエンド開発初心者ガイド

TypeScriptでミドルウェアの依存注入を行う設計方法
TypeScriptでミドルウェアの依存注入を行う設計方法

先生と生徒の会話形式で理解しよう

生徒

「TypeScriptとExpressを使ってバックエンドの開発に挑戦しているのですが、ミドルウェアの依存注入という設計方法が難しくてよく分かりません」

先生

「バックエンド開発の壁ですね。依存注入は、プログラムの部品を外から組み込む仕組みのことで、テストや修正を簡単にするためにとても大切な設計技法です」

生徒

「パソコンの操作もあまり慣れていないので、基礎から丁寧に教えてもらえると嬉しいです」

先生

「もちろんですよ。仕組みや言葉の意味、具体的なプログラムの書き方まで、一つずつ順番に確認していきましょう」

1. バックエンド開発とExpressとミドルウェアの基本

1. バックエンド開発とExpressとミドルウェアの基本
1. バックエンド開発とExpressとミドルウェアの基本

まずは、今回登場する基本的な言葉の意味から解説します。パソコン初心者の方でもイメージしやすいように、身近な例を使って説明していきます。

バックエンド開発とは、ウェブサイトやスマートフォンのアプリの裏側で動く仕組みを作るシステム開発のことです。例えば、インターネットで買い物をするときに、商品の在庫を確認したり、パスワードが合っているかをチェックしたりする見えない仕組みを指します。これに対して、画面に見える部分を作ることをフロントエンド開発と呼びます。

Node.jsは、本来はパソコンの画面上で動くウェブブラウザの中でしか動かなかったプログラミング言語であるJavaScriptを、パソコンのパソコン自体やサーバーという場所で動かせるようにした土台のような仕組みです。これにより、裏側の仕組みを記述することが可能になりました。

TypeScriptは、JavaScriptというプログラミング言語に「型」というルールを追加した、より安全で間違いの少ないプログラミング言語です。「型」とは、プログラムが扱うデータが文字なのか、数字なのかをあらかじめ厳しく決めておく約束事のことです。この約束があるおかげで、開発中に文字を入れるべき場所に間違えて数字を入れてしまった場合などに、パソコンがすぐに間違いを指摘してくれます。

Expressとは、Node.jsやTypeScriptを使ってバックエンドの開発をするときに、より簡単に、短い記述でプログラムを書けるように手助けしてくれる道具箱のようなものです。このような道具箱のことを専門用語でフレームワークと呼びます。Expressを使うことで、特定のURLにアクセスがあったときの処理を簡単に書くことができます。

そして、今回の中心となるミドルウェアとは、特定の処理を行う前に実行される、共通のお手伝いプログラムのことです。例えば、遊園地のアトラクションに乗る前に、係員さんがチケットを持っているか確認する様子をイメージしてください。この「アトラクションに乗る前に行うチケットの確認作業」こそが、バックエンド開発におけるミドルウェアの役割です。具体的には、ユーザーがログインしているかどうかの確認や、誰がいつアクセスしてきたかを記録する処理などに使われます。

2. 依存注入(DI)とは何かを簡単な例えで理解する

2. 依存注入(DI)とは何かを簡単な例えで理解する
2. 依存注入(DI)とは何かを簡単な例えで理解する

次に、今回の最大のテーマである依存注入について解説します。英語ではDependency Injectionと書くため、それぞれの頭文字をとってDIと呼ばれることが一般的です。

「依存」という言葉は、プログラミングの世界では「ある部品が動くために、別の特定の部品が必要不可欠な状態」を意味します。例えば、任天堂のゲーム機であるニンテンドースイッチを思い浮かべてみてください。もしゲーム機本体の中に、最初から特定のゲームソフトが直接埋め込まれていて、取り外しができない状態だったらどうでしょうか。そのゲームに飽きて別のゲームを遊びたくなっても、ゲーム機本体ごと買い替えなければならなくなります。この状態を「ゲーム機が特定のゲームソフトに強く依存している」と言います。

一方で、実際のゲーム機はゲームカードを差し替えることで、様々なゲームを自由に遊ぶことができます。ゲーム機本体は「ゲームカードを差し込む溝」だけを用意しておき、外から遊びたいゲームソフトを差し込みます。このように、特定の部品をあらかじめ内部に固定するのではなく、外から差し込んで組み合わせる仕組みのことを、プログラミングでは依存注入と呼びます。

バックエンドの開発において依存注入を行う理由は、プログラムの変更やテストが圧倒的に簡単になるからです。例えば、本物のデータを保存する仕組みの代わりに、テスト用の偽物のデータを保存する仕組みを外から差し替えることが可能になります。これにより、安全にプログラムが正しく動くかを確かめることができるようになります。

3. 依存注入をしない場合のミドルウェアの課題

3. 依存注入をしない場合のミドルウェアの課題
3. 依存注入をしない場合のミドルウェアの課題

依存注入の良さを知るために、まずは依存注入を使わずに、プログラムの中に直接特定の処理を書き込んでしまった場合の悪い例を見てみましょう。ここでは、アクセスしてきたユーザーの情報を確認するログ記録ミドルウェアのプログラムを考えます。

以下のプログラムは、特定のログを記録する処理をミドルウェアの内部で直接作成してしまっている状態です。これによって、後からログの記録先を変更したくなったときに、ミドルウェアのプログラム自体を書き直さなければならなくなります。


import express, { Request, Response, NextFunction } from 'express';

class SimpleLogger {
    log(message: string) {
        console.log("ログを出力します: " + message);
    }
}

const logger = new SimpleLogger();

function badMiddleware(req: Request, res: Response, next: NextFunction) {
    logger.log("ユーザーがアクセスしてきました");
    next();
}

const app = express();
app.use(badMiddleware);

app.get('/', (req: Request, res: Response) => {
    res.send("トップページです");
});

このプログラムの問題点は、badMiddlewareというミドルウェアが、SimpleLoggerという特定のログ記録クラスに完全に依存してしまっている点です。もし、画面にログを出すのではなく、ファイルにログを保存する別の仕組みに変えたいとなった場合、このミドルウェアのコードを直接改造しなければなりません。これは、先ほど例に挙げた「ゲームソフトが中に埋め込まれていて外せないゲーム機」と同じ状態になってしまっています。

4. インターフェースを使った部品の共通ルールの作成

4. インターフェースを使った部品の共通ルールの作成
4. インターフェースを使った部品の共通ルールの作成

依存注入を正しく行うためには、外から差し込まれる部品がどのような形をしているかをあらかじめ決めておく必要があります。この「部品の共通の設計図やルール」のことを、TypeScriptではインターフェースと呼びます。インターフェースは、クラスというプログラムの塊が必ず持っていなければならないボタンや機能を定義するものです。

先ほどのゲーム機の例で言えば、ゲームカードの縦の長さや横の長さ、端子の位置などの規格がインターフェースに当たります。この規格に従って作られていれば、どんなゲームソフトであってもゲーム機に差し込んで遊ぶことができます。

まずは、ログを記録する部品が必ず持っていなければならないルールを、インターフェースを使って定義してみましょう。


interface ILogger {
    writeLog(text: string): void;
}

class ConsoleLogger implements ILogger {
    writeLog(text: string) {
        console.log("画面への出力: " + text);
    }
}

class FileLogger implements ILogger {
    writeLog(text: string) {
        console.log("ファイルへの書き込みを想定: " + text);
    }
}

ここでは、ILoggerというインターフェースを定義しました。このインターフェースは、「writeLogという名前の、文字を受け取って処理する機能を必ず持っていなさい」というルールを定めています。そして、そのルールに従って、画面に出力するConsoleLoggerと、ファイルに書き込むことを想定したFileLoggerという二つの異なる部品を作りました。どちらも同じルールに従っているため、同じように扱うことができるのが特徴です。

5. 関数を使ってミドルウェアに依存注入を行う方法

5. 関数を使ってミドルウェアに依存注入を行う方法
5. 関数を使ってミドルウェアに依存注入を行う方法

それでは、インターフェースを使って作った部品を、ミドルウェアの外部から流し込む処理を作成してみましょう。まずは一番シンプルで分かりやすい、関数を使った依存注入の方法を解説します。関数とは、特定の処理を一つにまとめて名前をつけたものです。

ミドルウェアの関数を直接作るのではなく、「必要な部品を受け取って、ミドルウェアの関数を新しく作り出して返す関数」を作成します。このように、関数を返す関数のことを専門用語で高階関数と呼びます。初心者の方には少し難しく聞こえるかもしれませんが、実際のプログラムを見ると仕組みがよく分かります。


import { Request, Response, NextFunction } from 'express';

interface ILogger {
    writeLog(text: string): void;
}

function createLoggingMiddleware(loggerComponent: ILogger) {
    return function(req: Request, res: Response, next: NextFunction) {
        loggerComponent.writeLog("アクセスを検知しました");
        next();
    };
}

このcreateLoggingMiddlewareという関数は、引数としてILoggerというルールの部品を外から受け取ります。そして、受け取った部品を使って動く、Express専用のミドルウェアの関数をその場で組み立てて外に返しています。これで、ミドルウェアの内部に特定のログ記録処理を直接書き込む必要がなくなり、外から自由に部品を入れ替えられるようになりました。

6. クラスを使ってミドルウェアに依存注入を行う方法

6. クラスを使ってミドルウェアに依存注入を行う方法
6. クラスを使ってミドルウェアに依存注入を行う方法

先ほどは関数を使った方法を紹介しましたが、今度はクラスという仕組みを使って依存注入を行う方法を解説します。クラスとは、データと処理を一つの大きなグループとしてまとめるための設計図のようなものです。

クラスを使った依存注入では、クラスを新しく組み立てる瞬間に、必要となる部品を外から引き渡します。この組み立てを行う瞬間の処理のことをコンストラクタと呼びます。コンストラクタに部品を渡すことで、クラスの内部にあるすべての処理でその部品を自由に使えるようになります。


import { Request, Response, NextFunction } from 'express';

interface ILogger {
    writeLog(text: string): void;
}

class AuthMiddlewareProvider {
    private logger: ILogger;

    constructor(injectedLogger: ILogger) {
        this.logger = injectedLogger;
    }

    public handle(req: Request, res: Response, next: NextFunction) {
        this.logger.writeLog("認証の処理を実行中");
        next();
    }
}

このプログラムでは、AuthMiddlewareProviderというクラスを作りました。このクラスが作られるときに、constructorの部分で外からログ記録の部品を受け取り、クラスの内部にあるthis.loggerという場所に大切に保管します。そして、実際にミドルウェアとして動くhandleという処理の中で、保管しておいた部品を使用しています。このようにクラスを使うと、複数の異なる処理で同じ部品を使い回すことが簡単になります。

7. 実際に依存注入したミドルウェアをExpressに組み込む全体の流れ

7. 実際に依存注入したミドルウェアをExpressに組み込む全体の流れ
7. 実際に依存注入したミドルウェアをExpressに組み込む全体の流れ

部品の作り方と、それを受け取るミドルウェアの準備が整いましたので、最後にこれらをすべて組み合わせて、実際に動く一つのバックエンドプログラムを組み立ててみましょう。ここでは、最初に作った画面出力用の部品と、新しく作ったミドルウェアを合体させてExpressのアプリケーションを動かします。

以下のプログラムは、全体の組み立てを行うメインの処理になります。プログラムの最初の方で部品を作り、それをミドルウェアに流し込んでいる様子に注目してください。


import express, { Request, Response, NextFunction } from 'express';

interface ILogger {
    writeLog(text: string): void;
}

class ConsoleLogger implements ILogger {
    writeLog(text: string) {
        console.log("ログ画面出力: " + text);
    }
}

function createLoggingMiddleware(loggerComponent: ILogger) {
    return function(req: Request, res: Response, next: NextFunction) {
        loggerComponent.writeLog("ユーザーがサイトを訪問しました");
        next();
    };
}

const app = express();

const myLogger = new ConsoleLogger();

const dynamicMiddleware = createLoggingMiddleware(myLogger);

app.use(dynamicMiddleware);

app.get('/home', (req: Request, res: Response) => {
    res.send("こんにちは!バックエンドの世界へようこそ!");
});

console.log("サーバーが起動しました");

プログラムの流れを説明します。まず、画面に文字を出す役割を持ったConsoleLoggerから、myLoggerという実体の部品を作ります。次に、その部品をcreateLoggingMiddleware(myLogger)という形で関数の引数として中に流し込み、専用のミドルウェアを作成しています。最後に、Expressのapp.useという仕組みを使って、作成したミドルウェアをアプリケーション全体に登録しています。これで、特定のURLにアクセスがあったときに、外から注入されたログ記録処理が自動的に実行されるようになります。

8. 完成したプログラムを実行したときの出力結果の確認

8. 完成したプログラムを実行したときの出力結果の確認
8. 完成したプログラムを実行したときの出力結果の確認

作成したプログラムを実際にパソコンで実行したときに、画面にどのように表示されるかを確認してみましょう。バックエンド開発では、プログラムが正しく動いているかを判断するために、画面に出力される文字情報をよく確認します。

プログラムを起動し、インターネットブラウザを開いて該当のページにアクセスすると、パソコンの管理画面には以下のような結果が出力されます。


サーバーが起動しました
ログ画面出力: ユーザーがサイトを訪問しました

このように、外から注入したConsoleLoggerの仕組みがミドルウェアの中でしっかりと働き、文字が画面に表示されていることが確認できます。もし、ファイルに書き込む別の部品を注入していれば、この出力結果は自動的にファイルへの書き込み処理へと切り替わることになります。プログラムの本体を一切書き換えることなく、外側の組み合わせを変えるだけで動作を変更できるのが、この設計の最大の強みです。

関連記事:
カテゴリの一覧へ
新着記事
New1
JavaScript
JavaScriptのスコープとは?ローカル変数・グローバル変数の違いと使い分け
New2
TypeScript
TypeScriptで型の合成時に発生する型エラーの対処法を徹底解説!初心者にもわかりやすく解説
New3
TypeScript
TypeScriptでAPI呼び出しをラップするユーティリティ関数の設計完全ガイド!初心者向けにAxiosやFetchの使い方を解説
New4
TypeScript
TypeScriptのMapped Typesとインターフェースの連携方法を徹底解説!初心者でも理解できる型変換の基本
人気記事
No.1
Java&Spring記事人気No1
JavaScript
JavaScriptのループで無限ループが起きる原因と対策を初心者向けに徹底解説!
No.2
Java&Spring記事人気No2
JavaScript
JavaScriptのインストール方法まとめ!Windows・Mac・Linux別にステップ解説
No.3
Java&Spring記事人気No3
JavaScript
JavaScriptの文字列の長さを取得する(lengthプロパティ)
No.4
Java&Spring記事人気No4
TypeScript
TypeScriptのファイル分割とimport/exportの基本を徹底解説!初心者でも安心のやさしい入門
No.5
Java&Spring記事人気No5
TypeScript
TypeScriptでコメントを書く正しい書き方と使い分け【初心者向けにやさしく解説】
No.6
Java&Spring記事人気No6
TypeScript
TypeScriptでパスエイリアスを設定する方法!baseUrlとpathsでコードをスッキリ整理
No.7
Java&Spring記事人気No7
TypeScript
TypeScriptの始め方:開発環境の構築手順【初心者向け】
No.8
Java&Spring記事人気No8
JavaScript
JavaScriptでクエリパラメータを取得する方法を完全解説 URLSearchParamsの使い方を初心者向けに丁寧に説明