TypeScriptとExpressで非同期処理!async/awaitを使った初心者向けルート設計完全ガイド
生徒
「ウェブサーバーを作って、データベースからデータを読み込む仕組みを作りたいです。でも、データの読み込みを待っている間に、他の処理が止まってしまうと困るのですが、何か良い方法はありますか?」
先生
「そんなときには、TypeScriptとNode.js、そしてExpressという枠組みを組み合わせて、非同期処理という仕組みを使います。具体的には、asyncとawaitという命令を使って、ルート設計を行います。」
生徒
「非同期処理やルート設計、asyncやawaitといった言葉は、パソコンを触ったことがない自分には少し難しそうです。具体的にはどのようにプログラムを書けば良いのでしょうか?」
先生
「パソコンの操作に慣れていない方でも、身近な例えを使って一つずつ丁寧に説明します。それでは、基本的な使い方とプログラムの書き方を順番に見ていきましょう!」
1. バックエンド開発とExpressの基本を知ろう
私たちが普段、スマートフォンやパソコンでインターネットのページを見るとき、画面の裏側では情報を管理するコンピューターが動いています。この裏側の仕組みを作ることをバックエンド開発と呼びます。今回は、JavaScriptという言葉を進化させたTypeScriptを使って、裏側の仕組みを構築していきます。
バックエンド開発を行うためには、Node.jsという土台となる仕組みが必要です。さらに、その土台の上でウェブサーバーの機能を簡単に作れるようにしてくれる道具箱のような存在がExpressです。この道具箱を使うことで、特定のボタンが押されたときや、特定のページが開かれたときに、どのようなお返事を返すかを決めることができます。
初めてパソコンに触る方は、まず「画面の裏側で動くシステムを作っているのだな」というイメージを持ってください。TypeScriptは、プログラムの書き間違いを事前に教えてくれる親切な機能がついているため、初心者が安全にバックエンド開発を学ぶための最適な選択肢となります。
2. ルート設計とは何かを分かりやすく解説
ウェブサーバーを作るときに最も大切な考え方の一つがルート設計です。ルートとは、英語で「道」や「経路」を意味します。インターネットの世界におけるルート設計とは、訪問者が「どのURLにアクセスしたか」によって、案内する部屋を切り替える道案内のような役割を持ちます。
例えば、お買い物サイトを想像してください。商品の案内ページ、購入手続きのページ、問い合わせのページなど、目的によってURLの末尾が変わります。Expressでは、このURLの道筋を「ルート」と呼び、それぞれの道筋に対してどのような処理を実行するかをあらかじめ計画して記述しておく必要があります。これがルート設計です。
もしこのルート設計が正しく行われていないと、訪問者がページを開こうとしたときに「道が見つかりません」というエラー画面が表示されてしまいます。訪問者が迷子にならないように、どの道に来たらどのデータを渡すかというルールを明確に定義することが、バックエンド開発者の最初のお仕事になります。
3. 同期処理と非同期処理の違いを身近な例で例える
プログラムの動き方には、大きく分けて同期処理と非同期処理の二つの種類があります。これらを理解するために、お料理の注文を例にして考えてみましょう。まず、同期処理とは「一つの仕事が完全に終わるまで、次の仕事を絶対に始めない」という頑固な動き方です。ラーメン屋さんで、一人のラーメンが完成して席に届くまで、次の人の調理を一切始めない状態です。これでは大行列ができてしまいますね。
一方で、今回の主役である非同期処理は「時間がかかる仕事を誰かに任せている間に、自分は別の仕事をどんどん進める」という効率的な動き方です。ファミリーレストランでお肉を焼いている間に、別の店員さんがサラダを盛り付けたり、ドリンクバーの準備をしたりするイメージです。お肉が焼き上がるのをじっと待つ必要はありません。
バックエンド開発では、データベースという大きな倉庫からデータを引っ張ってきたり、他のウェブサイトから最新の天気の情報を取得したりするときに、どうしても待ち時間が発生します。この待ち時間の間にサーバー全体がピタッと止まってしまうのを防ぐために、非同期処理という仕組みが絶対に欠かせないのです。
4. 非同期処理を支えるPromiseの仕組み
非同期処理をプログラムで扱うとき、必ず登場するのがPromiseという言葉です。これは日本語に直訳すると「約束」という意味になります。プログラムの世界におけるPromiseは、「今はまだデータが準備できていないけれど、未来のいつか、必ず成功したデータか、あるいは失敗したという結果を返します」という約束手形のようなものです。
お肉の焼き上がりを待つ間、店員さんは注文を受け付けたというレシートを持っています。このレシートがPromiseです。お肉が無事に焼き上がれば、レシートと引き換えに美味しい料理が手に入ります。これが約束の「成功」です。もしお肉が焦げてしまって出せなくなったら、「失敗しました」というお詫びの結果が届きます。
TypeScriptでは、この約束の状態を厳格に管理することができます。データがまだ届いていない状態、無事に届いた状態、何らかのトラブルでエラーになった状態の三つを明確に区別することで、予期せぬ不具合のない頑丈なウェブシステムを組み立てることが可能になります。
5. 魔法の言葉asyncとawaitの使い方
非同期処理や約束の仕組みは便利ですが、昔の書き方だとプログラムが非常に複雑で見づらくなってしまう問題がありました。そこで登場したのが、まるで同期処理のように上から下へとすっきりと読めるようにしてくれる魔法の言葉、asyncとawaitです。
使い方はとてもシンプルです。まず、非同期処理を含んでいる特別な関数を作るために、関数の手前にasyncと書きます。これは「この関数の中では、時間のかかる処理を待つことができますよ」という合図です。そして、実際に時間がかかる処理を実行する命令の手前にawaitと書きます。これは「ここでデータが届くまで少し待機します」という意味になります。
この二つの言葉は必ずペアで使います。asyncがついていない関数の中でawaitを使うことはできません。このルールを守ることで、複雑なパズルを解くような非同期処理のプログラムが、まるで小説を上から順番に読むかのような、初心者にも分かりやすい綺麗な見た目に生まれ変わります。
6. 最初のシンプルなルートを書いてみよう
それでは、実際にExpressを使った基本的なルート設計のプログラムを見てみましょう。まずは、非同期処理を使わない、最も単純なお返事を返すだけの仕組みです。訪問者がトップページにアクセスしたときに、歓迎のメッセージを出力させます。
import express, { Request, Response } from 'express';
const app = express();
app.get('/', (req: Request, res: Response) => {
res.send("ウェブサーバーへようこそ!これは最初のルートです。");
});
app.listen(3000, () => {
console.log("サーバーがポート3000で起動しました。");
});
上記のプログラムを実行すると、パソコンの内部でウェブサーバーが立ち上がります。画面に文字を表示するための準備として、裏側では以下のような結果が出力されます。
サーバーがポート3000で起動しました。
プログラムの中にあるapp.getという部分が、ルート設計の根幹です。スラッシュマークはトップページを表しており、そこへアクセスが来たら、即座にお返事を送信するという道案内を記述しています。Requestは訪問者からの要求、Responseはサーバーからのお返事を意味する大切な用語です。
7. asyncとawaitを組み込んだ非同期ルート設計
次に、本題である非同期処理を組み込んだルート設計に挑戦しましょう。データベースからユーザーの情報を探して持ってくる、という時間を要する処理を想定したプログラムです。ここで先ほど学んだ魔法の言葉が活躍します。
import express, { Request, Response } from 'express';
const app = express();
const fetchUserData = (): Promise<string> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("田中太郎さんのデータがみつかりました。");
}, 2000);
});
};
app.get('/user', async (req: Request, res: Response) => {
console.log("データの取得を開始します。");
const result = await fetchUserData();
res.send(result);
});
このプログラムでは、データを準備するまでに2秒間の待ち時間が発生するように作られています。しかし、asyncとawaitのおかげで、サーバーはフリーズすることなく処理を待ち、準備ができた段階でお返事を返します。実行時の黒い画面には以下のように出力されます。
データの取得を開始します。
app.getの直後にasyncが配置されている点に注目してください。これにより、ルートの処理全体が非同期対応になります。そして、2秒かかるfetchUserDataの手前でawaitを使って待機することで、無事にデータが変数に格納されてから、次の行のお返事へと進むことができるのです。
8. エラーが起きたときのための例外処理対策
インターネットの世界では、いつも100パーセント通信が成功するとは限りません。データベースの電線が抜けてしまったり、目的のデータが完全に消えてしまっていたりすることもあります。そうした不測の事態が起きたとき、何も対策をしていないと、ウェブサーバー自体がクラッシュして完全に停止してしまいます。
非同期処理でエラーを上手に捕まえるために、tryとcatchという防護服のような仕組みを使います。tryのブロックの中に「もしかしたら失敗するかもしれない処理」を記述し、もしエラーが発生した場合は、即座にcatchのブロックへと処理がジャンプします。これにより、重大なエラーが起きてもサーバーを安全に動かし続けることができます。
パソコンを触ったことがない方でも、「失敗したとき用の予備のルートを準備しておく安全装置」だと考えれば理解しやすいはずです。プロのバックエンド開発者が作るシステムでは、この安全装置が必ず張り巡らされています。
9. 安全装置tryとcatchを導入した実践プログラム
それでは、エラーが発生する可能性を考慮した、より実践的で安全な非同期ルート設計のプログラムを書いてみましょう。今回は、商品のデータを取得する処理で、わざとエラーを発生させて、それを安全に処理する流れを記述します。
import express, { Request, Response } from 'express';
const app = express();
const fetchProductData = (): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("データベースへの接続に失敗しました。"));
}, 1500);
});
};
app.get('/product', async (req: Request, res: Response) => {
try {
const product = await fetchProductData();
res.send(product);
} catch (error) {
console.error("重大な問題を検知しました。");
res.status(500).send("申し訳ありません。サーバーでエラーが発生しました。");
}
});
このプログラムを実行すると、データ取得に失敗したというエラーの情報が安全に捕まえられ、サーバーの画面には以下のような警告が出力されます。サーバーが突然終了することはありません。
重大な問題を検知しました。
tryの中に書かれたawait fetchProductDataが失敗した瞬間に、プログラムは即座に下のcatchへと避難します。訪問者に対しては、システムが壊れた生々しいエラー画面を見せるのではなく、「申し訳ありません」という丁寧なお詫びのメッセージを返せるようになります。これが思いやりのあるルート設計です。
10. 複数の非同期処理を同時に効率よく動かす応用
最後のステップとして、少し発展的な技をご紹介します。ウェブサーバーを運営していると、一人の訪問者に対して「プロフィールの情報」と「最近の投稿一覧」など、複数の異なるデータを別々の場所から同時に持ってきたい場面に遭遇します。
これらを一つずつ順番に待っていると、全体の待ち時間が足し算で増えてしまい、ページが表示されるまでに長い時間がかかってしまいます。そこで、複数の非同期処理を同時にスタートさせて、全員が同時にゴールするのを待つPromise.allという強力な武器を使用します。
import express, { Request, Response } from 'express';
const app = express();
const getPrice = () => new Promise(resolve => setTimeout(() => resolve("100円"), 1000));
const Stock = () => new Promise(resolve => setTimeout(() => resolve("残り3個"), 1000));
app.get('/dashboard', async (req: Request, res: Response) => {
try {
const [priceResult, stockResult] = await Promise.all([getPrice(), Stock()]);
res.send(`価格は ${priceResult} で、在庫は ${stockResult} です。`);
} catch (error) {
res.status(500).send("同時取得中にエラーが発生しました。");
}
});
この仕組みを使うと、別々に待てば2秒かかる処理が、同時に並行して実行されるため、わずか1秒で完了するようになります。サーバーの処理結果は、以下のように一つの文章に綺麗にまとめられて出力されます。
価格は 100円 で、在庫は 残り3個 です。
配列という箱の中に、実行したい非同期処理を並べてPromise.allに渡すだけで、自動的に並列処理が行われます。この技術を習得することで、大量のアクセスが集まる本格的なウェブアプリケーションでも、軽快に動作する素晴らしいバックエンドシステムを設計できるようになります。