TypeScript × Expressまとめ!型安全かつ保守性の高いバックエンド設計ガイド
生徒
「TypeScriptとExpressを使ってWebサーバーを作りたいのですが、型安全や保守性の高い設計というのは具体的にどういう意味ですか?」
先生
「TypeScriptを使うと、データの種類を明確にする型定義ができるため、開発中のエラーを事前に防ぐことができます。ExpressというWebサーバーを作る道具と組み合わせることで、エラーが起きにくく、後からの改造もしやすい、安全で頑丈なバックエンドの仕組みを作ることができるんですよ。」
生徒
「プログラミングが初めての私でも、型安全なバックエンド開発の基本を理解して進められますか?」
先生
「大丈夫です!パソコンの仕組みや専門用語の意味を一つずつ丁寧にかみ砕きながら、実際の設計方法まで順番に学んでいきましょう!」
1. バックエンド開発とExpressの基本知識
インターネットの世界では、私たちがスマートフォンやパソコンの画面で目にする部分をフロントエンドと呼び、その裏側でデータを管理したり計算を行ったりする仕組みをバックエンド、またはサーバーサイドと呼びます。例えば、ネットショッピングで商品を検索したときに、倉庫の在庫データを調べて画面に送り返す役割を持っているのがバックエンドです。
このバックエンドを作るための土台として、世界中で広く使われているのがNode.jsという仕組みです。そして、そのNode.jsの上で、ウェブサイトの案内係のような仕組みを簡単に作れるようにしてくれる道具箱がExpressというフレームワークです。フレームワークとは、よく使う機能をあらかじめまとめてくれた便利な組み立てキットのようなものです。Expressを使うことで、特定のURLにアクセスがあったときに、どのようなデータを返すかというルールを少ない行数で分かりやすく書くことができます。
パソコンを触ったことがない方に向けて例えるなら、Node.jsはレストランという建物そのものであり、Expressは効率よく注文をさばくための有能な店長のような関係です。この二つを組み合わせることで、世界中のユーザーからのリクエストに素早く応じるWebアプリケーションの基盤を作ることができます。
2. TypeScriptを導入する理由と型安全のメリット
通常、Node.jsやExpressではJavaScriptというプログラミング言語が使われます。JavaScriptは柔軟で書きやすい反面、データの種類を厳密に区別しないため、文字を入れるべき場所に数字が入ってしまっても、プログラムを動かすまで間違いに気づきにくいという弱点があります。これを、事前のチェックで防いでくれるのがTypeScriptです。
TypeScriptの最大の特徴は、データに型という名札をつける点にあります。文字には「文字列型」、数字には「数値型」という専用の名札を貼ることで、間違った種類のデータを渡そうとした瞬間に、パソコンが「そこはデータの手違いがあります」と赤線を引いて教えてくれます。このように、データの種類が正しく保たれている状態のことを型安全と呼びます。
型安全であることは、システムの保守性、つまり「後からの改造や修正のしやすさ」を圧倒的に高めます。数ヶ月後にプログラムを書き換えるとき、どのデータがどんな形をしているかが一目で分かるため、迷子になることがありません。プログラミング未経験の方にとって、一見すると覚えることが多くて難しそうに感じるかもしれませんが、初期の段階でタイポや勘違いによるエラーをすべてパソコンが指摘してくれるため、初心者こそ導入すべき心強い味方なのです。
3. 開発環境の準備と初期設定の手順
TypeScriptを使ってExpressのプログラムを書くためには、まずパソコンの中に開発環境という作業部屋を作る必要があります。難しく聞こえるかもしれませんが、いくつかの決まった命令を実行するだけで自動的に準備が整います。まず、プロジェクトの情報を管理する設定ファイルを作成し、必要な道具をインストールします。
作業では、パソコンに指示を出すための黒い画面である「ターミナル」や「コマンドプロンプト」を使います。ここに文字を入力してエンターキーを押すことで、世界中のライブラリ、つまり先人が作った便利なプログラムの部品をダウンロードしてくることができます。TypeScriptでExpressを動かす際には、Express本体だけでなく、TypeScriptがExpressのデータの形を理解できるようにするための「型定義ファイル」という専用の解説書も一緒にインストールする必要があります。
すべての準備が整うと、設定ファイルが作られ、プログラムを書き始めることができる状態になります。この設定ファイルには、TypeScriptの厳密さをどのくらいにするか、どのフォルダに完成したファイルを書き出すかといった、細かいルールを記述することができます。最初は自動で作成された初期状態のままで十分に進めることができます。
4. 初めてのTypeScriptによるExpressサーバー構築
それでは、実際に動く最小限のWebサーバーのプログラムを書いてみましょう。以下のコードは、ウェブブラウザからアクセスがあったときに、画面に歓迎のメッセージを表示するシンプルな仕組みです。記述するファイル名は「server.ts」とします。末尾の「.ts」はTypeScriptのファイルであることを示す目印です。
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('TypeScriptとExpressの世界へようこそ');
});
app.listen(port, () => {
console.log('サーバーが起動しました');
});
このプログラムを実行すると、パソコンの内部で臨時のWebサーバーが立ち上がります。ブラウザのアドレスバーに特定の番号を入力してアクセスすると、以下のような結果が出力されます。
TypeScriptとExpressの世界へようこそ
上記のコードを詳しく解説します。1行目では、Expressの機能と一緒に、RequestとResponseという型を読み込んでいます。これがTypeScriptならではの重要なポイントです。Requestはユーザーから届いた「お願い」のデータ、Responseはサーバーから返す「お返事」のデータです。これらに型を指定することで、お返事を書くときに存在しない命令を使ってしまうミスを、プログラムを実行する前に完全に防ぐことができます。
5. 型安全を高めるリクエストデータの受け取り方
Webサーバーの重要な役割の一つに、ユーザーが入力したデータを受け取って処理することがあります。例えば、ユーザーのID番号を受け取って、その人のプロフィール情報を返すような場合です。しかし、ユーザーが常に正しいデータを送ってくるとは限りません。数字を期待しているのに文字が送られてくることもあります。そこで、受け取るデータの形をはじめに定義しておくことが大切です。
TypeScriptでは、interfaceという機能を使って、データの設計図をあらかじめ定義することができます。これを行うことで、送られてきたデータが設計図通りの形をしているかを厳密にチェックできるようになります。次のプログラムは、ユーザーの番号を受け取り、それに応じたメッセージを返す仕組みの例です。
import express, { Request, Response } from 'express';
const app = express();
interface UserParams {
userId: string;
}
app.get('/user/:userId', (req: Request<UserParams>, res: Response) => {
const userId = req.params.userId;
res.send('ユーザー識別番号は ' + userId + ' です');
});
app.listen(3000);
このプログラムに対して、例えば「/user/555」というURLでアクセスを送った場合、実行結果として画面には以下のように表示されます。
ユーザー識別番号は 555 です
このように、Requestの後に大なり小なりの記号を使って「UserParams」という設計図を当てはめることで、paramsの中には必ずuserIdという文字が入っていることをTypeScriptが保証してくれます。開発者が誤って別の名前でデータを呼び出そうとすると、エラーが発生して間違いを教えてくれるため、勘違いによるバグを未然に防ぐことができます。
6. 保守性を高めるルーティングの分割設計
システムが大きくなっていくと、一つのファイルにすべてのURLの処理を書き続けるのは不可能です。ファイルが何千行にもなってしまい、どこに何が書いてあるか分からなくなってしまいます。そこで、URLのジャンルごとにファイルを小分けにして整理する仕組みが必要になります。この、URLの道案内を分担させる仕組みをExpressではルーターと呼びます。
例えば、商品の情報を扱うURLのグループと、ブログの記事を扱うURLのグループを、それぞれ別のファイルに独立させます。こうすることで、商品に関するプログラムを直したいときは商品のファイルだけを見ればよくなり、他の場所に間違って影響を与えてしまう危険性が減ります。これが保守性の高い設計の第一歩です。次のプログラムは、商品用の案内図を別ファイルとして独立させる書き方の例です。
import express, { Router, Request, Response } from 'express';
const itemRouter = Router();
itemRouter.get('/list', (req: Request, res: Response) => {
res.send('商品の一覧データを表示します');
});
export default itemRouter;
この独立した案内図を、大元のメインファイルで読み込んで合体させることにより、全体の動きをきれいに整理することができます。このプログラムが呼び出されたときの実行結果は以下のようになります。
商品の一覧データを表示します
このように、機能ごとにファイルを綺麗に小分けにしておくことで、複数人のチームで開発するときも作業がバッティングせず、スムーズに開発を進めることができるようになります。
7. ミドルウェアを活用した共通処理の共通化
Webサーバーを作っていると、すべてのアクセスに対して共通で実行したい処理が出てきます。例えば、「誰がいつアクセスしてきたかの記録を帳簿に残す」機能や、「ログインしていないユーザーを追い返す」機能などです。これらをすべてのURLの処理に一つずつ書いていくのは面倒ですし、修正があったときに全部の場所を書き直さなければならなくなります。
Expressには、リクエストが目的地に到着する前に、途中で割り込んで特定の処理を実行させることができるミドルウェアという仕組みがあります。関所や検問所のようなイメージです。このミドルウェアも、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);
app.get('/info', (req: Request, res: Response) => {
res.send('インフォメーション画面です');
});
app.listen(3000);
このプログラムを起動し、ブラウザから該当のページにアクセスすると、画面に文字が表示されると同時に、サーバー側の黒い画面に以下のログが出力されます。
アクセスを検知しました
コード内にある「NextFunction」という型が重要です。これは、自分の検問所の処理が終わったら、次の処理へバトンを渡すための特別な関数です。このnextという命令を呼び出すことで、プログラムが途中で止まることなく、スムーズに次の案内係へと処理が引き継がれます。ミドルウェアを活用することで、同じコードを何度も書く必要がなくなり、すっきりと洗練された設計を実現できます。
8. エラーハンドリングの重要性と実装方法
プログラムは、常に予想通りに動くとは限りません。データベースとの通信が途切れてしまったり、予期せぬおかしなデータが送り込まれたりしたとき、プログラムが突然真っ白になって停止してしまうのは困ります。万が一エラーが起きてしまったときでも、システム全体がクラッシュしないように優しく受け止め、ユーザーには「ただいま不具合が発生しております」と丁寧な案内を返す仕組みが必要です。これをエラーハンドリングと呼びます。
Expressには、エラー専用のミドルウェアを用意する仕組みがあります。普通の処理とはルートを分け、エラーが発生したときだけ自動的に呼び出される特別な救護室のような場所を作っておくのです。ここでもTypeScriptの型定義が威力を発揮します。発生したエラーがどのような情報を持っているのかを明確にしながら、安全に処理を行うことができます。次のプログラムは、意図的にエラーを起こしたときに、救護室がそれをキャッチして安全なメッセージを返す例です。
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.get('/error-test', (req: Request, res: Response, next: NextFunction) => {
const customError = new Error('重大な問題が発生しました');
next(customError);
});
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error(err.message);
res.status(500).send('サーバー内部でエラーが起きました。しばらくお待ちください。');
});
app.listen(3000);
このテスト用のURLにアクセスすると、プログラムが異常終了することなく、ブラウザには以下の安全なお返事だけが返ってきます。
サーバー内部でエラーが起きました。しばらくお待ちください。
エラーが発生した際に、nextの中にエラーの情報を入れて呼び出すことで、Expressは自動的に一番下に用意されているエラー処理用のミドルウェアへと処理をジャンプさせます。これにより、エラーのログを裏でしっかりと記録しつつ、表向きの画面には親切なメッセージを出すという、プロフェッショナルで保守性の高いシステムを作ることができるようになります。