Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

【TechSelect+ 教材】8週間で学ぶWebアプリ開発入門

0
Posted at

この教材は、Web開発を初めて学ぶ大学生を対象とした8週間の入門教材です。プログラミング経験は問いません。

学習では、個々の製品名や構文を先に暗記するのではなく、Webアプリを「画面」「処理」「保存」の三つの役割に分け、データがどのように流れるかを確認します。その後、各役割を担当する技術を導入します。

この教材で扱うWebアプリとは、ブラウザ上で利用者の入力を受け取り、処理や保存を行うプログラムです。情報を読むことが中心のWebページに対し、入力・検索・保存などの操作を提供するものを、ここではWebアプリと呼びます。

チュートリアルで作るもの

共通課題として、次の機能を持つ「あいさつ保存アプリ」を作ります。

名前を入力して「保存」を押すと、名前が保存され、保存番号付きの挨拶が画面に表示される。

技術名を使わずに処理の流れを表すと、次のようになります。

  1. ブラウザの画面で名前を入力する
  2. 画面から、処理を担当するプログラムへ名前を送る
  3. 受け取った名前が正しいか確認する
  4. 保存を担当するプログラムへ名前を記録する
  5. 保存番号と挨拶を画面へ返す

この流れが、教材全体を通して確認するWebアプリの基本構造です。題材は、処理を受け付ける場所と、保存するデータのまとまりを1つずつに限定します。正式な用語は第1章で説明します。機能を絞ることで、コードと設定の関係を追いやすくしています。

ここに書いてある単語わからないものありますか?

領域 学ぶ内容
コンピューターの基礎 プログラム、プロセス、OS、メモリ、ストレージ、ファイル、パス
Webの基礎 ネットワーク、クライアント、サーバー、データベース、HTTP、JSON
プログラミング JavaScriptとTypeScriptの基本文法、型、非同期処理
フロントエンド 画面の構造、コンポーネント、状態管理、フォーム
バックエンド API、入力検証、HTTPレスポンス、エラー処理
データ保存 テーブル、SQL、制約、永続化
開発環境 ターミナル、Git、Docker、複数サービスの起動
開発プロセス AIを利用した調査・実装・レビューと、人による検証

具体的な技術名は、第0章でコンピューターの基礎を確認した後、第1章で担当範囲と対応させながら紹介します。

この教材の進め方

最初に読む第0〜1章、前半の「8週間カリキュラム」、後半の「詳細リファレンス」で構成されています。最初から全文を通読する必要はありません。

  1. 第0章で、ファイル、実行中のプログラム、メモリ、通信などの基礎を確認する
  2. 第1章で、画面・処理・保存の役割を確認する
  3. その週の目標、作業、到達チェックを読む
  4. 実装中に必要になった項目だけ、詳細リファレンス第2〜16章で確認する
  5. 第5〜7週は共通アプリを土台に個人制作を行う
  6. 第8週は公開環境へデプロイし、成果と学習過程を振り返る

第4週までの到達目標は、資料を見ずにすべて再現することではありません。必要な資料を参照しながら、技術の役割を説明し、アプリを起動・確認できる状態を目指します。

目次

第0章:Web開発のためのコンピューターサイエンス基礎

Web開発では、コードの文法だけでなく、「保存されたファイル」「実行中のプログラム」「通信相手」「一時的な値」「停止後も残るデータ」を区別する必要があります。この章では、後の学習に必要なコンピューターサイエンスの最小範囲を確認します。

コンピューターサイエンスは広い分野です。この教材では、CPUの内部構造、2進数の計算、アルゴリズムの計算量、ネットワーク規格の詳細までは扱いません。用語の暗記ではなく、後で起きる現象を説明できることを目標にします。所要時間の目安は45〜60分です。

この章の終了時に、次の違いを説明できれば十分です。

  • ソースコードと、実行中のプロセス
  • メモリ上の一時的な値と、ストレージへ永続化したデータ
  • ファイルパスとURL
  • クライアントとサーバー
  • ホストとポート
  • 同じプログラム内の値と、外部から届くデータ

説明は三つのまとまりに分かれています。

範囲 主題
0-1〜0-5 1台のPCでファイルとプログラムを扱う基礎
0-6〜0-8 Web通信と外部データを扱う基礎
0-9〜0-10 問題の調べ方と理解の確認

0-1. プログラム、プロセス、OS

プログラムは、コンピューターに処理を行わせる命令のまとまりです。人が読める形で書いたプログラムをソースコードと呼びます。

ソースコードは、最初はストレージ上にファイルとして保存された文字データです。ストレージは停止後もデータを残す保存領域、ファイルは名前を付けて保存する単位です。0-3と0-4で詳しく説明します。

ファイルを保存しただけでは、新しいプログラムが起動するわけではありません。ただし、すでに動いている開発ツールが変更を監視し、保存をきっかけに再処理する場合はあります。

プログラムを起動し、実際に処理している一つの実体をプロセスと呼びます。同じプログラムを2回起動すれば、通常は別々のプロセスとして動きます。

OS(オペレーティングシステム) は、プロセス、処理中の値を置くメモリ、保存されたファイル、画面、ネットワークなどを管理し、プログラムが利用する機能を提供します。Windows、macOS、LinuxはOSの例です。

ストレージ上のソースコード
          ↓ 実行環境が読み込む
OSが実行環境のプロセスを動かす
          ↓ コードを必要な形へ変換・実行する
プロセスがメモリを使って処理する
          ↓ 停止する
そのプロセスの実行は終わる

実行環境は、コードを読み込み、必要に応じて変換しながら実行するプログラムです。OSがソースコードそのものを直接プロセスにするのではありません。第1章では、ブラウザやNode.jsなど、今回使う実行環境を区別します。

開発サーバーを起動したターミナルが入力待ちへ戻らないのは、そのターミナルでプロセスが動き続けているためです。Ctrl + Cは、そのターミナルで実行中の処理へ中断を伝えます。

ここでいうOS上の「プロセス」は、作業の進め方を表す「開発プロセス」とは別の意味です。

0-2. 入力・処理・出力とデータ

多くのプログラムは、入力を受け取り、決められた手順で処理し、結果を出力します。問題を解くための手順をアルゴリズムと呼びます。

入力             処理                         出力
名前「Taro」  →  空でないか確認して挨拶を作る  →  「こんにちは、Taroさん!」

入力はキーボードだけではありません。ファイルから読んだ内容、別のプログラムから届いた通信、時刻、DB(データベース)から取得した行も、プログラムにとっては入力です。出力も画面表示だけではなく、ファイルへの保存、通信相手への応答、ログへの記録などがあります。

名前を付けるなどして呼び出せる、まとまった処理の単位を関数と呼びます。関数は必要に応じて引数を受け取り、戻り値を返します。引数や戻り値を使わない関数もあります。関数を実行することを、関数を呼び出すと表現します。具体的な構文は第1週と詳細リファレンス第2章で扱います。

データには値と形があります。

データの形 主な用途
文字列 "10""Taro" 名前や文章
数値 10 件数や保存番号
真偽値 truefalse 条件が成立するか
値の並び 複数の名前 一覧
項目名付きのまとまり 名前と保存番号 関連する値をまとめる

文字列の"10"と数値の10は見た目が似ていますが、異なるデータです。文字列には文字列の処理、数値には計算を適用します。

補足:ビット、バイト、文字エンコーディング

コンピューターはデータを最終的に01の組み合わせで扱います。0または1の一つをビット、一般に8ビットをまとめた単位をバイトと呼びます。

文字とバイトの対応方法が文字エンコーディングです。この教材では、Webで広く使われるUTF-8を使います。第1週のHTMLに書く<meta charset="UTF-8" />は、文字エンコーディングをブラウザへ伝える指定です。

変数は、プログラムから値を参照するために付ける名前です。一般的な意味での状態は、プログラムがある時点で保持している値の組み合わせを指します。具体的な変数と型の書き方は詳細リファレンス第2章で扱います。

第2週では、Reactが画面用に管理する状態を、先頭を大文字にしたStateと表記します。ReactのStateを変更すると画面が再レンダーされます。詳しい仕組みは詳細リファレンス第9章で扱います。

0-3. メモリ、ストレージ、永続化

メモリは、動作中のプロセスが値や処理途中の状態を置く領域です。プロセスが終了すると、そのプロセスだけがメモリに持っていた値は通常失われます。

ストレージは、SSDなど、ファイルを継続して保存する領域です。プロセスを停止・再起動しても必要なデータが残る性質を永続化と呼びます。

保存場所 停止後 この教材での例
プロセスのメモリ 通常は失われる 入力途中の文字、DB導入前の一時的な配列
ストレージ上のファイル 残る ソースコード、設定ファイル
DBが管理するデータファイル 残るように構成する 第4週以降に保存する名前

「画面から消えたか」と「保存データが消えたか」も別です。画面を再表示すると、保存先から同じデータを読み直せる場合があります。

0-4. ファイル、フォルダー、パス、現在地

ファイルは、コードや設定などを名前付きで保存する単位です。ファイルをまとめる場所を、この教材ではフォルダーと呼びます。技術資料に出てくるディレクトリも、ここでは同じ意味です。

ファイル名の末尾にある.ts.tsx.jsonなどを拡張子と呼びます。拡張子はファイルの用途や形式を判断する手掛かりです。

パスは、ファイルやフォルダーの場所を表します。ファイルシステムの起点から場所をすべて示すものが絶対パス、現在いるフォルダーを基準に示すものが相対パスです。

後で作るプロジェクトの一部を例にします。

greeting-db-app/              ← プロジェクトルート
├── frontend/
│   └── src/
│       └── App.tsx
├── backend/
└── database/

LinuxやWSLでの/home/student/greeting-db-app/frontend/src/App.tsxは、先頭の/から場所を示す絶対パスの例です。プロジェクトルートにいるときのfrontend/src/App.tsxは相対パスです。..は一つ上のフォルダー、.は現在のフォルダーを表します。

ターミナルには 現在地(カレントディレクトリ) があります。同じコマンドでも、現在地が違えば対象になるファイルが変わります。この教材で「プロジェクトルートから実行する」と書かれている場合は、上のgreeting-db-appへ移動してから実行します。

ファイルパスと、Web上の場所を表すURLのパスは別のものです。名前は似ていますが、基準と利用するプログラムが異なります。

0-5. ターミナル、シェル、コマンド

ターミナルは、文字でコンピューターを操作するための画面です。入力された文字をコマンドとして解釈し、OSへ処理を依頼するプログラムをシェルと呼びます。この教材ではbashまたはzshと互換性のあるコマンドを使います。

command-name   target-file
└コマンド名┘   └引数(対象ファイル)┘
  • コマンド: 実行する処理の名前
  • 引数: コマンドへ渡す対象や値
  • オプション: 動作を切り替える指定。--versionなど

画面に何も表示されない場合でも、必ず失敗とは限りません。反対に、途中で結果が表示されても、最後にエラーで終了する場合があります。最初のエラーと、コマンドが最後まで終了したかを確認します。

補足:標準出力、標準エラー出力、終了ステータス
  • 標準出力: 通常の実行結果を文字で出す基本的な経路
  • 標準エラー出力: エラーや警告を文字で出す基本的な経路
  • 終了ステータス: コマンドの成否を示す数値。一般に0は成功、0以外は失敗

例えば、第1章で紹介するNode.jsのnode --versionがバージョン番号を表示して正常終了すれば、標準出力と成功の終了ステータスを確認できます。

存在しないファイルをnode missing.jsで指定すると、Node.jsはエラーを表示し、失敗を示す終了ステータスで終わります。教材では、単に文字が表示されたかだけでなく、最後にエラーがないかも確認します。

実際のpwdlscdなどの操作は第1週で練習します。

ここまでの0-1〜0-5では、主に1台のPCの中でファイルとプログラムを扱う基礎を確認しました。0-6〜0-8では、複数のプログラムが通信するWeb開発へ範囲を広げます。最初は用語を暗記せず、矢印のどちら側にいるかを追ってください。

0-6. クライアント、サーバー、プロトコル

別々のプロセスは、OSの機能やネットワークを使ってデータを交換できます。処理やデータを要求する側をクライアント、要求を待ち受けて結果を返す側をサーバーと呼びます。これは製品名ではなく、1回の通信における役割です。

クライアント ── リクエスト(要求) ──> サーバー
クライアント <─ レスポンス(応答) ── サーバー

同じプログラムでも、通信相手によって役割が変わります。ブラウザから要求を受ける処理担当プログラムは、ブラウザに対してサーバーです。そのプログラムが保存担当プログラムへ問い合わせるときは、保存担当プログラムに対するクライアントになります。

通信する双方がデータの意味を理解するには、形式と手順を共有する必要があります。この通信上のルールをプロトコルと呼びます。Webで使うHTTPはプロトコルの一つです。

「サーバー」は、要求へ応答するプログラムを指す場合と、そのプログラムを動かすコンピューターを指す場合があります。この教材では、文脈に応じて「サーバープログラム」「サーバー用コンピューター」と区別します。

0-7. IPアドレス、ホスト名、ポート、URL

ネットワーク通信では、「どのコンピューターの、どのプログラムの、どの機能へ接続するか」を指定します。

用語 役割
IPアドレス ネットワーク上の接続先を表す番号
ホスト ネットワークへ接続し、通信の送り先または受け手になるコンピューター
ホスト名 接続先を人が扱いやすい名前で表す
ドメイン名 インターネット上の範囲や接続先を表す名前。example.comなど
DNS 一般に、ドメイン名とIPアドレスを対応付ける仕組み
localhost 接続を始めるプログラムが属するネットワーク環境自身を指す特別なホスト名
ポート 同じホスト上で待ち受ける複数のプログラムを区別する番号
URL 通信方法、ホスト名、ポート、パスなどをまとめて指定する表記
http://localhost:3000/api/greetings
└方式┘ └─ホスト名─┘└ポート┘└──パス──┘

PC上のブラウザから見たlocalhostは、そのPCを指します。一方、後で使うコンテナという隔離された実行単位では、内部のプログラムから見たlocalhostはそのコンテナ自身です。別のコンテナへ接続するときはlocalhostではなく、第4週で学ぶComposeのサービス名を使います。

この教材では、PCから接続する5173番を画面の開発用、3000番をAPI用に使います。番号が同じでも、接続先のホストが異なれば別の通信先です。

URLの/api/greetingsは、サーバー内の機能を指定するパスです。0-4で説明したファイルパスとは別であり、同じ名前のファイルがあることを意味しません。

0-8. プロセスの境界と入力検証

同じプロセス内では、関数へ値を直接渡せます。別のプロセスへ値を送る場合は、文字列やバイト列など、双方が読み書きできる形へ変換して通信します。第1章では、Webアプリで使う具体的な形式としてJSONを紹介します。

ネットワークやファイルから届く値は、常に期待した形とは限りません。項目がない、文字列ではない、長すぎる、壊れた形式である、といった場合があります。処理前にデータの型、必須項目、長さ、許可する範囲を確認することを入力検証と呼びます。

プログラム内部で作った値
  └─ コードの規則や型検査で誤りを見つける

ネットワークやファイルなど外部由来の値
  └─ 変数へ入れた後も、実行時検証が済むまでは信頼しない

値が現在どのプロセスにあるかではなく、どこから来て、必要な実行時検証が済んでいるかが重要です。後で使う型検査はコードを確認しますが、別のプログラムや保存領域から実際に届くデータまでは自動的に検証しません。第1章以降では、画面、処理、保存の各段階で必要な確認を行います。

入力検証は安全性の一部ですが、それだけで利用者の本人確認や操作権限の管理ができるわけではありません。

0-9. 問題を層ごとに調べる

Webアプリは複数の層から構成されます。動作しないときに全ファイルを一度に変更すると、どの変更が結果へ影響したか分からなくなります。

保存したソースコード
  ↓
実行中のプロセス
  ↓
ホストとポート
  ↓
リクエストとレスポンス
  ↓
メモリまたはストレージ

次の順で観察します。

  1. 何を実現したいか、期待する結果を一文で書く
  2. 実際の結果と、最初に表示されたエラーを記録する
  3. 現在地、対象ファイル、保存済みかを確認する
  4. 必要なプロセスが起動しているか確認する
  5. 接続先のホストとポートを確認する
  6. リクエスト、レスポンス、ログを確認する
  7. 保存先にデータがあるか確認する
  8. 一度に一つだけ変更し、同じ手順でもう一度試す

エラーは、原因を調べるための観察結果です。エラーメッセージを消すこと自体ではなく、期待した処理が成立することを確認します。

メンターに相談する目安

上の順序で一度確認しても次に見る場所を決められない場合は、「目的」「期待した結果」「実際の結果」「最初のエラー」「試したこと」を整理してメンターへ相談してください。答えだけでなく、どの層を次に確認すべきかを質問します。パスワード、接続URL、トークンは共有しません。

0-10. 確認テスト

次の問いに、資料を見ながら自分の言葉で答えてください。

  1. ソースコードのファイルと、実行中のプロセスは何が違いますか。
  2. メモリ上の一覧と、ストレージへ保存したデータは、プロセス停止後にどうなりますか。
  3. プロジェクトルートからfrontend/src/App.tsxを指定するパスは、絶対パスと相対パスのどちらですか。
  4. ブラウザが処理担当プログラムへ要求するとき、どちらがクライアントですか。処理担当プログラムが保存担当プログラムへ問い合わせるときはどうですか。
  5. 同じPCで画面用とAPI用のプログラムを同時に動かすとき、ポートは何を区別しますか。
  6. コード内部の型が正しくても、外部から届くデータを実行時に検証する必要があるのはなぜですか。
回答例を確認する
  1. ソースコードはストレージへ保存された命令で、プロセスはそのプログラムが実際に動いている一つの実体です。
  2. プロセスだけがメモリに持つ一覧は通常失われます。ストレージへ永続化したデータは、削除しない限り再起動後も読み直せます。
  3. プロジェクトルートという現在地を基準にしているため、相対パスです。
  4. 最初はブラウザがクライアント、処理担当プログラムがサーバーです。保存担当プログラムへ問い合わせるときは、処理担当プログラムがクライアント側になります。
  5. 同じホスト上で通信を待ち受ける別々のプログラムを区別します。
  6. コードの型や規則は、実行中に外部から届く値の実体までは保証しないためです。

次の第1章では、この基礎をWebアプリの「画面」「処理」「保存」へ対応させ、各役割を担当する技術を紹介します。

第1章:Webアプリの全体像と技術の役割

この章では、最初にWebアプリの仕組みを確認し、次に各技術が担当する役割を整理します。用語は、実装で使うたびに確認すれば十分です。

1-1. ブラウザが表示する三つのもの

Chrome、Safari、Firefox、Edgeなど、Webページを開くアプリをブラウザと呼びます。ブラウザのアドレス欄へURLを入力すると、ブラウザはURLで指定された場所へ内容を要求し、返された内容を画面に表示します。この要求と応答を、リクエストとレスポンスと呼びます。

一般的なWebページの画面側は、主に三つの技術でできています。

名前 一言でいうと
HTML ページの構造 見出し、入力欄、ボタン
CSS 見た目 色、余白、文字サイズ
JavaScript 動きや処理 クリックへの反応、通信、表示の変更

今回、見た目を整えるCSSはほとんど扱いません。HTMLに似た記法で画面の構造を作り、JavaScriptを基にした言語で動きを付けます。

1-2. Webアプリの三つの役割

この教材では、Webアプリを次の三つに分けて考えます。

役割 すること 今回の例
画面 利用者の入力を受け取り、結果を表示する 名前の入力欄と保存ボタン
処理 入力を確認し、保存を依頼して結果を返す 空の名前を拒否し、挨拶を作る
保存 データを後から読める形で記録する 名前と保存番号を残す

画面側をフロントエンド、処理側をバックエンドと呼びます。保存にはデータベースを使います。

フロントエンド       バックエンド          データベース
入力と表示     →     検証と処理      →     データを保存
ブラウザで動く       サーバーで動く        サーバーから利用する

第0章で確認したように、サーバーは通信上の役割です。この教材で「バックエンド」と書く場合は、主にブラウザからの要求へ応答するサーバープログラムを指します。

1-3. フロントエンドとバックエンドの通信

フロントエンドとバックエンドは、関数を直接呼び合うのではなく、ネットワーク越しに通信します。

ブラウザ
  └─ リクエスト(要求) ──> バックエンド
  <─ レスポンス(応答) ──┘
  • HTTP: リクエストとレスポンスを交換するための通信ルール
  • JSON: 通信するデータを、文字列として表す形式
  • API: プログラム間で機能やデータを交換するために公開されたインターフェース
  • エンドポイント: HTTPメソッドとURLパスで識別されるAPIの接続先

今回は、画面から次のJSONを送ります。

{
  "name": "Taro"
}

nameは項目名、Taroは値です。バックエンドはPOST /api/greetingsというエンドポイントで受け取ります。POSTは「データを送って処理・保存を依頼する」ときによく使うHTTPメソッドです。

HTTPの詳細は第3週で扱います。ここでは、フロントエンドがリクエストを送り、バックエンドがレスポンスを返すことを確認してください。

1-4. 技術の種類を整理する

ここから具体的な技術名を紹介します。その前に、技術の種類を整理します。

  • プログラミング言語: 人が処理を書くための文法とルール
  • 実行環境: コードを読み込み、必要に応じて変換しながら実行するプログラム
  • ライブラリ: よく使う処理を再利用できるようにまとめた部品
  • フレームワーク: アプリの作り方や土台を提供する仕組み
  • 開発ツール: 開発サーバー、変換、ビルドなど開発作業を支援するソフトウェア

ライブラリとフレームワークの境界は、資料によって表現が異なることもあります。最初は分類を暗記するより、「どこで何を担当するか」を確認してください。

1-5. フロントエンドの技術

画面側では、次の三つを組み合わせます。

TypeScriptで画面の処理を書く
  ↓
Reactで画面をコンポーネントに分ける
  ↓
Viteがブラウザで実行できる形へ変換して配信する
  • TypeScript: JavaScriptに型の検査を加えたプログラミング言語
  • React: データから画面を作るためのライブラリ
  • Vite: 開発中のファイル配信や、本番用ファイルのビルドを行う開発ツール

ReactとViteは同じ役割ではありません。Reactは画面の作り方を担当し、Viteはそのコードを開発中に配信・変換する作業を担当します。最終的にブラウザが実行するのはJavaScriptです。

1-6. バックエンドの技術

処理側では、次の三つを使います。

TypeScriptで処理を書く
  ↓
HonoでHTTPの受付場所を作る
  ↓
Node.jsがプログラムを実行する
  • Node.js: ブラウザの外でJavaScriptを動かす実行環境
  • Hono: HTTPリクエストを受け取ってレスポンスを返すWebフレームワーク
  • pg: Node.jsのプログラムからPostgreSQLへ接続するライブラリ

Node.jsとHonoも同じ役割ではありません。OS上で動くNode.jsのプロセスがJavaScriptを実行し、Honoはその実行環境上でAPIを作りやすくする土台を提供します。

1-7. データ保存の技術

  • DB: Databaseの略。保存されたデータと、その構造をまとめたもの
  • DBMS: DBを管理するソフトウェア
  • PostgreSQL: 今回使うDBMSの製品名
  • SQL: DBMSへ「表を作る」「行を保存する」「行を読む」と伝える言語
バックエンド ── SQL ──> PostgreSQL ──> DBへ保存

PostgreSQLとSQLは同じものではありません。PostgreSQLは動くソフトウェア、SQLはそのソフトウェアへ命令を書くための言語です。

1-8. 開発環境を構成する技術

Webアプリでは、画面、バックエンド、DBという複数のプログラムを同時に動かします。この教材では、それらを起動しやすくするために次を使います。

  • Docker: プログラムを隔離された実行単位で動かすためのソフトウェア
  • コンテナ: Dockerで実際に起動している一つの実行単位
  • Docker Compose: 複数のコンテナの設定と起動手順を一つにまとめる仕組み
  • サービス: Composeで管理する一つの構成単位
  • Volume: コンテナを停止・作り直してもDBデータを残す保存領域

Dockerの詳しい仕組みは詳細リファレンス第3章で説明します。ここでは、画面用・処理用・保存用の3つのサービスをまとめて起動する技術として位置付けます。

インストールして利用できるようにまとめられたコードをパッケージと呼びます。パッケージの追加とバージョン管理には、npmというパッケージマネージャーを使います。npmはNode.jsをインストールすると一緒に利用できるようになります。

1-9. 今回、自分のPCで動くもの

第4週までは、アプリをインターネットへ公開せず、自分のPCの中だけで動かします。

第0章で確認したホスト、localhost、ポートを、今回の構成へ対応させます。

  • ホスト: この段階では、Dockerを動かしている自分のPC
  • localhost: ブラウザなどから見た、自分のPCという接続先
  • 5173番: 画面の開発サーバーを区別するポート
  • 3000番: APIサーバーを区別するポート

URLを分解すると次のようになります。

http://localhost:3000/api/greetings
└HTTP┘ └自分のPC┘└番号┘└機能の場所┘
  • http: 使う通信方法
  • localhost: 通信先のホスト名
  • 3000: バックエンドを区別するポート番号
  • /api/greetings: サーバー内の機能を示すパス

この教材では、Composeが三つのサービスを起動します。

サービス名 PCから開く場所 担当技術 役割
frontend http://localhost:5173 Vite Reactのファイルをブラウザへ配信する
backend http://localhost:3000 Node.jsとHono 入力を検証し、DBへ保存する
db PCへは公開しない PostgreSQL データを保存する

frontendコンテナ内で動くのはViteの開発サーバーです。Viteから受け取ったReactのJavaScriptは、利用者のブラウザで動きます。

アプリ全体の流れへ、ここまでの技術名を当てはめると次のようになります。

第4週完成時の処理フローを見る

HonoはVolumeへ直接書き込みません。HonoはPostgreSQLへSQLを送り、PostgreSQLが自分のデータファイルをVolumeへ保存します。

1-10. 新しい用語を整理する方法

新しい用語が出たら、次の三点で整理します。

  1. 画面、処理、保存、開発環境のどこで使うか
  2. その技術へ何を渡すか
  3. その技術から何が返るか

例えばHonoなら「処理側で使う」「HTTPリクエストを渡す」「HTTPレスポンスが返る」と整理できます。この教材では、名前を暗記できたかではなく、役割とデータの流れを説明できることを目標にします。


第I部:全体像

第1〜4週は、全員が同じあいさつ保存アプリを段階的に作ります。第5週からは、その経験を使って自分で決めたアプリを作ります。

第4週までに、このREADME後半の「第1〜4週の詳細リファレンス」にある共通アプリを一通り理解・実行します。ここでいう理解は暗記ではなく、資料を参照しながら役割を説明し、もう一度動かせる状態です。

主題 その週の成果物
第1週 コンピューター基礎、ターミナル、Web、JavaScript・TypeScript、Git、Docker、AI駆動開発 練習コードと最初のGitコミット
第2週 フロントエンドとReact ブラウザ内だけで動くあいさつ画面
第3週 バックエンド、HTTP、API ReactからHonoへ通信するアプリ
第4週 PostgreSQL、SQL、Docker Compose 名前をDBへ永続保存する共通アプリ
第5週 個人制作の企画と設計 企画書、画面案、API・DB設計、骨組み
第6週 作成・一覧機能の実装 1つの機能が画面からDBまで動く版
第7週 個人制作の完成と品質確認 エラー処理・説明・テストを含む公開候補版
第8週 デプロイ、発表、振り返り 公開URL、発表、振り返り文書

メンターに相談する目安

確認手順を一度試しても進めない場合は、一人で抱え込む必要はありません。「目的」「期待した結果」「実際の結果」「試したこと」を整理してメンターへ相談してください。

OS、使用中のターミナル、現在地、実行したコマンド、エラー全文、関係するgit diffがあると、状況を共有しやすくなります。パスワードや接続URLなどのシークレットは共有しません。

各回の終了時は、npm run devを動かしているターミナルでCtrl + Cを押します。Composeを使う週は、プロジェクトルートでdocker compose downを実行します。

downだけならDBのVolumeは残ります。保存データも消すdocker compose down -vは、警告のある手順以外では使いません。

第4週までの到達目標

第4週の終わりに、次を自分の言葉で説明できることを目指します。

  • ブラウザ、バックエンド、DBの役割
  • ソースコードと、実行中のプロセスの違い
  • メモリ上の値と、ストレージへ永続化したデータの違い
  • ファイルパスとURLの違い
  • クライアント、サーバー、ホスト、ポートの関係
  • ReactとViteが同じものではない理由
  • Node.jsとHonoが同じものではない理由
  • PropsとStateの違い
  • HTTP、JSON、APIの役割
  • TypeScriptの型と実行時検証の違い
  • PostgreSQLとSQLの違い
  • Dockerイメージ、コンテナ、Compose、Volumeの違い
  • localhost:5173localhost:3000db:5432の使い分け
  • Gitコミットと、AIが提案した変更を自分で検証する理由

毎週行うGitとAIの記録

GitとAIは第1週だけの学習項目ではありません。各週で、次の作業を繰り返します。

目的を決める
  ↓
AIには調査・計画を先に依頼する
  ↓
小さな範囲だけ変更する
  ↓
git diffで変更を読む
  ↓
型検査・ビルド・HTTP・DB・画面で確認する
  ↓
説明できる変更だけコミットする

docs/ai-log.mdには、少なくとも次の内容を記録します。

## 日付と目的

- 使用したAI:
- 依頼した内容:
- 採用した提案:
- 採用しなかった提案と理由:
- 自分で実行した確認:
- 結果:
- 自分の言葉で説明できること:

CodexとClaude Codeを同じ未コミット状態へ同時に編集させないでください。比較する場合は、一方を実装、もう一方を編集なしのレビューにするか、Gitへコミットしてから切り替えます。


第1週:開発の土台を作る

第1週は、開発環境と基本操作の習得に集中します。アプリ本体の実装は第2週から始めます。

セッションを始める前に第0章第1章を読み、分からない用語へ印を付けてください。第1週の操作を通して同じ用語をもう一度確認するため、この時点で暗記する必要はありません。

第1週の目標

  • ターミナルで現在の場所を確認し、フォルダーを移動できる
  • ソースコード、プロセス、OSの関係を説明できる
  • メモリ上の一時的な値と、永続的な保存を区別できる
  • Webアプリの画面・処理・保存を区別できる
  • 短いJavaScriptを実行し、TypeScriptが何を加えるか説明できる
  • Gitで変更を記録できる
  • Dockerのイメージとコンテナを区別できる
  • CodexまたはClaude Codeへ小さな依頼を出し、差分を自分で確認できる

セッション1:必要なソフトウェアを準備する

次をインストールします。学校や職場のPCでインストール権限がない場合は、担当者の用意した環境を使ってください。

新しいPCへすべてを導入する場合は、60〜90分を超えることがあります。環境構築は第1週の事前準備・予備枠として扱い、必要に応じて授業開始前または複数回に分けます。導入後、同じターミナルで次の確認コマンドが動くことを到達基準とします。

  • Node.js: JavaScriptをPC上で実行する環境。サポート中のLTS版を選ぶ
  • Git: ファイルの変更履歴を記録するソフトウェア
  • Docker: コンテナを動かすソフトウェア
  • VS Codeなどのテキストエディター: コードを書くソフトウェア
  • Chrome、Firefox、Edge、Safariなどのブラウザ
  • CodexまたはClaude Codeのどちらか一つ: セッション5で使うコーディングエージェント

AIツールはセッション5で使用します。次のいずれかの公式ガイドに従い、このセッションでインストールと認証まで済ませます。

利用できるものを1つ選び、もう一方との比較は任意とします。

ターミナルを開き、次を一行ずつ実行します。

node -v
npm -v
git --version
curl --version
docker --version
docker compose version

バージョン番号が表示されれば、そのコマンドを実行できています。番号は教材と完全に同じでなくても構いません。command not foundと表示された場合は、インストールまたはターミナルの再起動を確認します。

バージョン番号は、第0章0-5で説明した標準出力の例です。command not foundは、シェルが指定されたコマンドを見つけられなかったことを伝えるエラーです。

この教材のコマンドは、macOS・Linuxのターミナル、またはWindows上のWSLで動くbashを前提とします。WSLは、Windows上でLinux環境を使う仕組みです。

Windowsでは、次の環境に統一します。

  1. MicrosoftのWSLインストール手順に従ってWSLを用意する
  2. Node.js、Git、curlをWindows側ではなくWSL内へ導入する
  3. この教材のコマンドとバージョン確認は、同じWSLターミナルで実行する
  4. Docker DesktopのWSL連携を有効にする

途中でPowerShellやGit Bashへ切り替えると、ファイルの場所や利用できるコマンドが変わります。学校などで指定環境がある場合は、その手順を優先してください。

メンターに相談する目安

公式手順、インストール、ターミナルの再起動を確認してもバージョン番号が表示されない場合は、同じ操作を繰り返す前にメンターへ相談してください。使っているOSとターミナル、実行したコマンド、表示されたエラー、確認できたバージョン番号を共有します。パスワードや認証コードは共有しません。

セッション2:ターミナルとフォルダー

第0章0-4と0-5で確認した、ファイル、パス、現在地、コマンドを実際に操作します。

ターミナルは、文字のコマンドでPCへ操作を依頼するアプリです。エディターはファイルの中身を編集し、ターミナルはプログラムの起動やフォルダー操作に使います。

pwd
ls
mkdir greeting-db-app
cd greeting-db-app
mkdir -p practice/week1 docs
pwd
ls
コマンド 意味
pwd 現在いるフォルダーの場所を表示する
ls 現在のフォルダーにあるものを表示する
mkdir 名前 フォルダーを作る
cd 名前 そのフォルダーへ移動する
cd .. 一つ上のフォルダーへ戻る
mkdir -p A/B 必要な親フォルダーも含めて作る

greeting-db-appが、8週間使うプロジェクトの一番上のフォルダーです。以降、これをプロジェクトルートと呼びます。

ターミナルでは「今どこにいるか」が重要です。コマンドが見つけるファイルは現在地によって変わります。迷ったらpwdlsを実行してください。

教材のコードブロックには、プロンプト記号の$を付けていません。表示されているコマンドだけを入力します。実行中の開発サーバーを止めるときはCtrl + Cを押します。

セッション3:HTML、JavaScript、TypeScript

最初に、ブラウザが表示するHTMLを一つ作ります。practice/week1/index.htmlを作ってください。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Web開発の第一歩</title>
    <style>
      body {
        font-family: sans-serif;
      }
    </style>
  </head>
  <body>
    <h1>はじめてのWebページ</h1>
    <p>HTMLが構造を作り、CSSが見た目を整えます。</p>
  </body>
</html>

ファイルマネージャーからindex.htmlをダブルクリックし、ブラウザで開きます。WindowsのWSLでファイルの場所を確認する場合は、プロジェクトルートでexplorer.exe .を実行すると、現在のフォルダーをWindows Explorerで開けます。

今回はファイルを直接開くため、URLはfile://...から始まります。第2週からはViteがhttp://localhost:5173でファイルを配信します。

  • <h1>: 最も大きな見出し
  • <p>: 段落
  • <meta charset="UTF-8" />: このHTMLの文字エンコーディングがUTF-8であるとブラウザへ伝える
  • <style>: この短い例ではCSSをHTML内へ書く場所
  • 開始タグと終了タグで内容を囲む

次にJavaScriptを実行します。

エディターでプロジェクトルートを開き、practice/week1/greeting.jsを作ります。

const userName = 'Taro'

function createGreeting(name) {
  return `こんにちは、${name}さん!`
}

console.log(createGreeting(userName))

コードは基本的に上から読み進められます。functionの行では関数を定義し、関数の中身は末尾でcreateGreeting(userName)を呼び出したときに実行されます。

  • const userName = 'Taro': 文字列'Taro'を参照する変数userNameを宣言する。constで宣言した変数へ別の値は再代入できない
  • function createGreeting(name): createGreeting関数を定義する。呼び出し時に渡された値を引数nameで受け取る
  • {}: 関数など、ひとまとまりの処理範囲を表す
  • return: 関数の処理を終え、呼び出し元へ値を返す
  • `こんにちは、${name}さん!`: 文字列の中へ${name}の値を埋め込むテンプレートリテラル
  • createGreeting(userName): userNameの値を渡して関数を呼び出す
  • console.log(...): 括弧内の値をターミナルへ表示する

プロジェクトルートのターミナルで実行します。

node practice/week1/greeting.js

期待する出力です。

こんにちは、Taroさん!

JavaScriptでは、nameへどの種類の値を渡すかをこの関数だけでは明示していません。TypeScriptでは型を書けます。次をTypeScript Playgroundへ貼り付けてください。

function createGreeting(name: string): string {
  return `こんにちは、${name}さん!`
}

console.log(createGreeting('Taro'))
  • name: string: 引数は文字列
  • ): string: 戻り値も文字列

同じPlaygroundで、上のコードの末尾に次を加えると、実行前に型エラーを確認できます。

createGreeting(123)

JavaScriptがブラウザやNode.jsで動く言語で、TypeScriptはJavaScriptへ型の検査を加えた言語です。第1週はこの違いだけ理解できれば十分です。詳しい文法は詳細リファレンス第2章で扱います。

セッション4:Gitで変更を記録する

Gitは、ファイルの状態を任意の時点で記録するバージョン管理システムです。Google Driveの同期や単なるバックアップとは異なり、「どの行を、なぜ変えたか」という開発履歴を管理します。

プロジェクトルートで開始します。

git init
git branch -M main
git status

初回のコミットで名前とメールアドレスを求められた場合は、自分の値をこのプロジェクトだけに設定します。

git config user.name "Your Name"
git config user.email "you@example.com"

エディターでプロジェクトルートに.gitignoreを作ります。

node_modules
dist
.env
.env.*
!.env.example
.DS_Store

.gitignoreは、Gitへ記録しないファイルを指定します。依存パッケージ、ビルド結果、秘密を含む.envなどを記録対象から外します。

git diff
git status
git add .
git status
git commit -m "chore: start eight-week web course"
git log --oneline
Git用語 意味
リポジトリ Gitで履歴を管理するプロジェクト
変更 前回の記録から変わった内容
ステージ 次のコミットへ含める変更を選んだ状態
コミット 変更内容へ説明を付けた一つの記録
ブランチ 履歴を分けて作業する線。第5週から使う

git addは保存ではなく、次のコミットへ含める変更を選ぶ操作です。git commitまで成功して初めてGitの履歴になります。

セッション5:DockerとAI駆動開発を体験する

Dockerの基本操作を確認した後、AIを利用して小さなコード変更を計画・実装・検証します。

Dockerの基本操作

Docker DesktopまたはDocker Engineを起動してから実行します。

docker run --rm hello-world

このコマンドでは、Dockerがhello-world イメージを取得し、そこからコンテナを起動します。メッセージを表示するとコンテナは終了し、--rmにより終了したコンテナは削除されます。

イメージ    = コンテナを作るための起動用ひな形
コンテナ    = イメージから実際に起動した一回分

イメージは起動用のひな形、コンテナはそのひな形から起動した実体です。Dockerfile、Compose、ネットワーク、Volumeは第4週で扱います。

AI支援開発の基本手順

本教材ではAI駆動開発を、人が目的と確認基準を決め、AIに調査・説明・変更案を支援させ、最後に人が差分と動作を検証する開発方法として扱います。

この教材では、コーディングエージェントの例としてCodexとClaude Codeを扱います。セッション1で準備した、利用できるいずれか一方を使って次の演習を行います。

  • Codex CLI公式ガイド: プロジェクトを調べ、編集し、コマンドを実行できるコーディングエージェント
  • Claude Code公式ガイド: ターミナルからコードベースを調査・変更できるコーディングツール

利用できるアカウント、プラン、学校・職場の規則は環境ごとに異なります。もう一方の画面や使い方を比べる作業は、時間に余裕がある場合だけ行います。

インストール後、プロジェクトルートからCodexならcodex、Claude Codeならclaudeを起動します。最初はファイルを変更させず、次のように依頼してください。

practice/week1/greeting.jsを読んでください。
まだファイルは変更せず、各行が何をしているか、
Web開発が初めての人向けに説明してください。

次に、小さな変更を計画だけさせます。

挨拶の末尾に「TypeScriptを学習中です」を追加したいです。
まだ編集せず、変更する行、変更理由、確認方法を説明してください。

計画を理解できたら、対象を限定して変更を依頼します。

practice/week1/greeting.jsだけを変更してください。
期待する出力は「こんにちは、Taroさん! TypeScriptを学習中です」です。
変更後にnodeで実行し、結果を報告してください。

AIが作業した後は、自分でも確認します。

git diff
node practice/week1/greeting.js
git status

AIと作業するときは、毎回次の順序を守ります。

  1. 目的を一文で伝える
  2. 変更するファイルや扱わない範囲を指定する
  3. 完了条件を、表示やコマンドの結果として伝える
  4. 大きな変更の前は、編集せず計画を出してもらう
  5. git diffで変更された行を読む
  6. ビルド、テスト、ブラウザで動作を確認する
  7. 理解できた変更だけをコミットする

次の情報はAIへの入力やGitのコミットに含めません。

  • パスワード、APIキー、アクセストークン
  • 本物の個人情報や機密データ
  • 学校や会社が外部共有を禁止しているコード・文書

AIの説明やコードは間違うことがあります。意味が分からないコマンド、ファイルを大量に消す操作、権限確認を無効にする操作は承認しません。分からない場合は「このコマンドが変更するものと、元に戻す方法を先に説明してください」と聞きます。

docs/ai-log.mdを作り、次を記録します。

# 第1週 AI作業ログ

## 依頼したこと

## AIが変更したこと

## 自分で確認したコマンドと結果

## 理解できたこと

## 未解決事項

最後に記録します。

git add .
git commit -m "docs: record first AI-assisted change"

第1週の到達チェック

  • pwdlsで現在地を確認できる
  • HTMLファイルをブラウザで開き、HTMLとCSSの役割を説明できる
  • JavaScriptのファイルをNode.jsで実行できる
  • JavaScriptとTypeScriptの違いを一文で説明できる
  • git statusgit diffgit addgit commitを使える
  • Dockerイメージとコンテナの違いを説明できる
  • CodexまたはClaude Codeへ、計画→小さな変更→検証の順で依頼できる
  • AIの変更をgit diffで確認してからコミットした

第1週では、コマンドや文法を暗記することより、「分からないときに現在地、差分、実行結果を確認できる」ことが重要です。


第2週:Reactによるフロントエンド実装

第2週はフロントエンドだけを作ります。まだAPIやDBは使いません。入力した名前をStateに保持し、ブラウザ内で挨拶へ変換して表示します。

  • JSX: TypeScriptの中で、HTMLに似た形で画面を書く記法
  • State: コンポーネント内部で保持し、更新時に再レンダーを発生させる値
  • Props: 親コンポーネントから子コンポーネントへ渡す値

今週はこの三語を、実際に入力して表示が変わる様子と結び付けます。

第2週の目標

  • ReactとViteの役割の違いを説明できる
  • JSXで入力欄とボタンを作れる
  • AppResultMessageの親子関係を説明できる
  • Stateを更新すると再レンダーされることを確認できる
  • Propsで親から子へ文字列を渡せる
  • フロントエンドのビルドを成功させる

第2週に読む場所

unknown、DB問い合わせ、CORSは、第3週以降で扱います。

セッション1:Reactプロジェクトを作る

第1週に作ったgreeting-db-appへ移動します。pwdlsで確認してから実行してください。

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm run dev -- --port 5173 --strictPort

ターミナルに表示されたURLをブラウザで開きます。通常はhttp://localhost:5173です。

  • create-vite: Viteが用意したプロジェクト生成ツール
  • frontend: 作るフォルダー名
  • react-ts: ReactとTypeScriptを使うテンプレート
  • npm install: 必要なパッケージをインストールする
  • npm run dev: 開発サーバーを起動する
  • --port 5173: 5173番を使う
  • --strictPort: 5173番が使用中なら別番号へ変えず、分かる形で停止する

バックエンドはhttp://localhost:5173からの通信を許可するため、開発画面の番号を5173へ固定します。5173番が使用中と表示されたら、別のViteが動いていないか確認して停止します。

package.jsonはプロジェクト名、コマンド、必要なパッケージを記録するファイルです。package-lock.jsonは実際に使うパッケージのバージョンを固定します。node_modulesはインストール済みパッケージの置き場所なので、直接編集しません。

確認後はCtrl + Cで開発サーバーを止め、プロジェクトルートへ戻ります。

cd ..

セッション2〜3:最初のコンポーネントを作る

frontend/src/App.tsxを開き、現在の内容をすべて削除して、次のコードへ置き換えます。以降、この操作を「全置換」と表記します。

import { useState } from 'react'
import type { FormEvent } from 'react'

type ResultMessageProps = {
  message: string
}

function ResultMessage(props: ResultMessageProps) {
  return <p>{props.message}</p>
}

function App() {
  const [name, setName] = useState('')
  const [message, setMessage] = useState('')

  function handleSubmit(event: FormEvent<HTMLFormElement>): void {
    event.preventDefault()
    setMessage(`こんにちは、${name}さん!`)
  }

  return (
    <main>
      <h1>あいさつアプリ</h1>

      <form onSubmit={handleSubmit}>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          value={name}
          maxLength={100}
          required
          onChange={(event) => setName(event.target.value)}
        />
        <button type="submit">表示</button>
      </form>

      {message !== '' && <ResultMessage message={message} />}
    </main>
  )
}

export default App

起動します。

cd frontend
npm run dev -- --port 5173 --strictPort

ブラウザで名前を入力し、「表示」を押します。通信していないため、バックエンドを起動しなくても表示されます。

入力する
  ↓ onChange
name Stateが変わる
  ↓ 表示ボタン
handleSubmitがmessage Stateを変える
  ↓ 再レンダー
message PropsがResultMessageへ渡る

ここまで確認したらCtrl + Cで止め、プロジェクトルートへ戻ります。

cd ..

セッション4:既存コードを変更し、挙動を検証する

次から一つ選びます。

  • 見出しを「あいさつ練習」へ変える
  • 挨拶へ「今日も学びましょう」を加える
  • 表示後にsetName('')を呼び、入力欄を空にする

プロジェクトルートから画面を起動し、動かしたままエディターで変更します。

cd frontend
npm run dev -- --port 5173 --strictPort

最初に自分で変更し、ブラウザで確認します。その後、AIへ次のようなレビューを依頼します。

frontend/src/App.tsxをレビューしてください。
まだ編集せず、AppとResultMessageの親子関係、
StateとPropsの流れ、初心者が誤解しそうな点を説明してください。

AIの説明と、詳細リファレンス第9章の説明を比べます。AIの説明だけを正解としません。

確認後はCtrl + Cで止め、プロジェクトルートへ戻ります。

cd ..

セッション5:ビルドとGit

cd frontend
npm run build
cd ..
git status
git diff
git add frontend
git commit -m "feat: build local React greeting form"

npm run buildは、TypeScriptの型検査と公開用ファイルの作成を行います。開発画面が動いても型エラーが残る場合があるため、毎週確認します。

メンターに相談する目安

画面が開かない、またはビルドに失敗し、最初のエラーを読んでも次の確認方法が分からない場合は、メンターへ相談してください。変更したファイル、期待した画面、ターミナルの出力、ブラウザのConsole、npm run buildgit diffの結果を共有します。完成コードそのものではなく、原因を切り分ける次の一手を質問してください。

第2週の到達チェック

  • npm run devで画面を開ける
  • 名前を入力して挨拶を表示できる
  • Appが親、ResultMessageが子だと説明できる
  • PropsとStateの違いを説明できる
  • ページ再読み込みと再レンダーの違いを説明できる
  • 自分で一か所変更した
  • npm run buildが成功する
  • 第2週の変更をGitへコミットした

第3週:HonoによるAPI実装とフロントエンドとの通信

第3週は、名前を処理するバックエンドを追加します。DBはまだ使わず、名前をバックエンドのメモリへ一時保存します。バックエンドを再起動すると消えることを確認し、第4週でDBが必要になる理由につなげます。

第3週の目標

  • フロントエンドとバックエンドの役割を区別できる
  • HTTPのリクエストとレスポンスを確認できる
  • HonoでPOSTエンドポイントを作れる
  • unknownの入力を実行時に検証できる
  • メモリ上の一時保存とDBの永続保存を区別できる
  • ReactのfetchからHonoへJSONを送れる
  • 正常時と失敗時を自分で試せる

第3週に読む場所

DBとpool.queryの部分は第4週に読みます。

セッション1:Honoプロジェクトを作る

プロジェクトルートで実行します。

npm create hono@latest backend -- --template nodejs --pm npm --install
  • backend: 作るフォルダー
  • nodejs: Node.js上でHonoを動かすテンプレート
  • --install: 必要なパッケージもインストールする

セッション2:DBを使わないAPIを作る

backend/src/index.tsの内容を、次のコードへ全置換します。

import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { cors } from 'hono/cors'

type SavedGreeting = {
  id: number
  name: string
}

const greetings: SavedGreeting[] = []

const app = new Hono()

app.use(
  '/api/*',
  cors({
    origin: 'http://localhost:5173',
    allowMethods: ['POST', 'OPTIONS'],
    allowHeaders: ['Content-Type'],
  }),
)

app.post('/api/greetings', async (c) => {
  const body: unknown = await c.req.json().catch(() => null)

  if (
    typeof body !== 'object' ||
    body === null ||
    !('name' in body) ||
    typeof body.name !== 'string'
  ) {
    return c.json({ error: '名前を文字列で送ってください' }, 400)
  }

  const name = body.name.trim()

  if (name === '' || name.length > 100) {
    return c.json({ error: '名前は1〜100文字で入力してください' }, 400)
  }

  const savedGreeting: SavedGreeting = {
    id: greetings.length + 1,
    name,
  }

  greetings.push(savedGreeting)

  return c.json(
    {
      id: savedGreeting.id,
      message: `こんにちは、${savedGreeting.name}さん!`,
    },
    201,
  )
})

serve(
  {
    fetch: app.fetch,
    port: 3000,
    hostname: '0.0.0.0',
  },
  (info) => {
    console.log(`API: http://localhost:${info.port}`)
  },
)

greetingsは、バックエンドが動いている間だけメモリ上に存在する配列です。1件目はid=1、2件目はid=2になりますが、バックエンドを再起動すると配列は空へ戻ります。これは学習用の一時保存であり、本番の保存方法ではありません。

cors(...)は、http://localhost:5173で開いたブラウザ画面がAPIのレスポンスを読めるようにする設定です。CORSはブラウザの通信制限に関する仕組みであり、ログインや利用者の権限を確認する機能ではありません。

バックエンドを起動します。

cd backend
npm run dev

別のターミナルを開き、APIだけを確認します。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"Taro"}'

curlは、ターミナルからHTTPリクエストを送るコマンドラインツールです。このコマンドを分解すると次の意味になります。

部分 意味
-i ステータス行とレスポンスヘッダーも表示する
-X POST POSTメソッドを使う
-H JSONを送ることをヘッダーで伝える
-d 続く文字列をリクエスト本文として送る
行末の\ bashで、一つのコマンドを次の行へ続ける

三行をまとめて一つのコマンドです。行末の\を入力したらEnterを押し、続きの行を入力します。教材に表示されていないプロンプト文字は入力しません。

201 Createdと挨拶のJSONが返れば成功です。空白の名前も試します。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"   "}'

こちらは400 Bad Requestになります。

JSONとして壊れた本文も試します。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":'

これも400 Bad Requestになります。TypeScriptの型検査は、実行時にネットワークから届くデータの内容を確認しません。そのため、Hono側でも入力を検証します。

メモリ保存の性質を、次の順に確認します。

  1. 正常なPOSTを2回送り、IDが12と増えることを確認する
  2. バックエンドをCtrl + Cで停止する
  3. backendフォルダーでnpm run devを再実行する
  4. 正常なPOSTをもう一度送り、IDが1へ戻ることを確認する

再起動でIDが戻るのは、配列の内容がメモリにしか保存されていないためです。

セッション3〜4:ReactからAPIを呼ぶ

frontend/src/App.tsxを開き、ResultMessagePropsの型定義の直後へ次を追加します。

type GreetingResponse = {
  id: number
  message: string
}

同じファイルにあるhandleSubmit関数の先頭から末尾までを、次の非同期関数に置き換えます。

この置き換えにより、ブラウザ内で挨拶を作る処理は、Honoから届いた挨拶を表示する処理へ変わります。

第2週の任意課題で、以前のhandleSubmitに加えた処理はいったん削除されます。まずAPIとの通信を完成させ、その後で必要な変更だけを新しい処理へ追加してください。例えば、保存後に入力欄を空にする場合は、成功時のsetMessage(...)の直後へsetName('')を追加します。

差し替え位置を確認できない場合は、詳細リファレンス第9章の完成版App.tsxでファイル全体を置き換えます。第3週と第4週でフロントエンドが受け取るJSONの形は同じです。

async function handleSubmit(
  event: FormEvent<HTMLFormElement>,
): Promise<void> {
  event.preventDefault()

  try {
    const response = await fetch(
      'http://localhost:3000/api/greetings',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
      },
    )

    if (!response.ok) {
      throw new Error(`API エラー: ${response.status}`)
    }

    const data = (await response.json()) as GreetingResponse
    setMessage(`保存番号 ${data.id}: ${data.message}`)
  } catch {
    setMessage('保存に失敗しました')
  }
}

画面の見出しを「あいさつ保存アプリ」、ボタンを「保存」へ変更します。この週の保存先はバックエンドのメモリで、再起動すると消えることを画面確認でも意識してください。

二つのターミナルを使います。ターミナルAでは、前節からbackendにいる状態でAPIを動かし続けます。ターミナルBは新しく開き、pwdlsでプロジェクトルートにいることを確認してからfrontendへ移動します。

ターミナルA: backend内                  → npm run dev
ターミナルB: プロジェクトルートでcd frontend → npm run dev -- --port 5173 --strictPort

ブラウザで名前を送ると、ReactがHonoへ通信し、Honoが作った挨拶を表示します。

次にバックエンドだけをCtrl + Cで止めてから、もう一度保存します。「保存に失敗しました」と表示されれば、失敗経路も確認できています。

確認後は、frontendを動かしているターミナルBでもCtrl + Cを押します。第3週の作業終了時には、backendfrontendの両方が停止している状態にします。これにより、第4週のComposeが3000番と5173番を使用できます。

メンターに相談する目安

Reactからの通信が失敗するときは、まずcurlでAPI単体を確認します。curlも失敗する場合はHTTPステータス、レスポンス、バックエンドのログを、curlは成功してブラウザだけ失敗する場合はNetworkタブとConsoleの結果をメンターへ共有してください。二つのターミナルのどちらで何を起動しているかも伝えます。

セッション5:ビルド、説明、Git

開発サーバーとは別のターミナルを開き、プロジェクトルートから両方をビルドします。

cd frontend
npm run build
cd ../backend
npm run build
cd ..

AIへコードレビューを依頼する場合は、修正より先に観点を指定します。

第3週のfrontendとbackendを、編集せずレビューしてください。
正常入力、空白入力、100文字超、壊れたJSON、backend停止時の
5ケースについて、現在の動作と確認方法を表にしてください。

自分でも表の各ケースを確認してからコミットします。

git add frontend backend
git commit -m "feat: connect React form to Hono API"

第3週の到達チェック

  • curlの正常入力で201が返る
  • 空白や不正なJSONで400が返る
  • ReactからHonoへ名前を送れる
  • Hono停止時に失敗メッセージが表示される
  • backend再起動後にIDが戻り、DBが必要な理由を説明できる
  • HTTP、JSON、APIを一文ずつ説明できる
  • TypeScriptの型とtypeofによる実行時検証の違いを説明できる
  • frontendbackendのビルドが成功する
  • 第3週の変更をGitへコミットした

第4週:DBとDocker Composeを加えて共通アプリを完成させる

第4週は、メモリだけに置いていた名前をPostgreSQLへ保存し、IDもPostgreSQLに発行させます。3つのサービスをDocker Composeで起動し、停止・再起動後も名前が残ることを確認します。

第4週の目標

  • DB、DBMS、PostgreSQL、SQLの違いを説明できる
  • テーブル、行、列、主キーを説明できる
  • CREATE TABLEINSERTSELECTを使える
  • Honoからパラメーター化クエリで保存できる
  • Dockerfile、イメージ、コンテナ、Composeを区別できる
  • Volumeによる永続化を確認できる
  • 第1〜4週の共通アプリ全体を説明できる

第4週に使う詳細リファレンス

今週は、後半の詳細リファレンスを実際の手順書として使います。

セッション 詳細リファレンス 作るもの
1 第3章 3-1〜3-4、続けて第5章 database/init.sqlgreetingsテーブル
2 第3章 3-5〜3-7、続けて第6〜7章 Dockerfile、.dockerignorecompose.yaml
3 第8章 pgを使う完成版Hono API
4 第9〜12章 完成版Reactとの接続、全体動作
5 第13〜14章、第16章 エラー調査、ビルド、永続化、まとめ。第15章の演習は任意

第2週でfrontend、第3週でbackendを作成済みです。詳細リファレンス第4章の同じ生成コマンドは再実行せず、不足するpgとDB用フォルダーだけ追加します。

新しいターミナルを開き、pwdlsでプロジェクトルートにいることを確認してから実行します。

cd backend
npm install pg
npm install --save-dev @types/pg
cd ..
mkdir -p database

その後は、詳細リファレンス第5〜10章の手順に沿って、init.sql、Dockerfile、compose.yamlindex.tsApp.tsxを順に完成版へ置き換えます。

今週の動作確認順

複数の問題を同時に調べないよう、DB→API→画面の依存関係順に確認します。

  1. DBだけ: テーブルが作られたか
  2. DBとAPI: curlで保存できるか
  3. 3サービス: ブラウザから保存できるか
  4. 永続化: docker compose downと再起動後も行が残るか
  5. ビルド: frontendbackendの型検査が成功するか

期待する最終経路です。

React
  └─ HTTP/JSON ─> Hono
                     └─ パラメーター化SQL ─> PostgreSQL
                                                └─ DBデータ ─> Volume

メンターに相談する目安

dbunhealthyまたはexitedのままなら、APIや画面の作業へ進む前にメンターへ相談してください。失敗したチェックポイント、実行したコマンド、docker compose psdocker compose logs dbで最初に表示されたエラーを共有します。

原因が分からない状態でdocker compose down -vは実行しません。

第4週のGitチェックポイント

変更を1つの大きなコミットへまとめず、動作を確認した層ごとに記録します。

git add database
git commit -m "feat: add PostgreSQL schema"

git add compose.yaml frontend/Dockerfile frontend/.dockerignore
git add backend/Dockerfile backend/.dockerignore
git commit -m "chore: run three services with Docker Compose"

git add backend
git commit -m "feat: persist greetings from Hono"

第3週で詳細リファレンス第9章の完成版App.tsxを使った場合、フロントエンドには第4週の差分がありません。git statusfrontendに差分がある場合だけ、次のコミットを追加します。

git add frontend
git commit -m "feat: complete shared greeting application"

コミット名は、実際に変更したファイルに合わせて調整してください。任意で第4週の地点へタグを付けます。

git tag week4-complete

タグは特定のコミットへ付ける目印です。個人制作中に共通アプリの完成状態を確認したくなったときに使えます。

第4週の到達チェック

  • docker compose configが成功する
  • dbhealthyになる
  • DB単体、API、画面の順で確認できる
  • 正常入力で201、空白と101文字以上で400が返る
  • psqlで保存された行を確認できる
  • docker compose down後もVolumeによりデータが残る
  • Reactが保存番号付きの挨拶を表示する
  • frontendbackendのビルドが成功する
  • 第1〜4週で登場した技術を、画面・処理・保存・環境へ分類できる
  • 共通アプリの完成コミットが残っている

第4週までで、後に続く詳細リファレンス第2〜16章で扱う共通アプリの主要部分を実行したことになります。資料を参照しながら役割を説明し、再実行できれば、この段階の到達基準を満たします。


第5週:自分が作りたいものを設計する

第5週から個人制作へ進みます。実装の前に、「誰が、何をしたいか」「第5〜7週でどこまで作るか」を決めます。

ここで作る最小の公開候補を MVP(Minimum Viable Product) と呼びます。この教材では「利用者が最も大切な一操作を、最初から最後まで完了できる最小版」という意味で使います。

第5週の目標

  • 解決したい小さな問題を一文で説明できる
  • 必須機能と、今回は作らない機能を分けられる
  • 画面、API、DBの対応を設計できる
  • 実装前に完了条件を決められる
  • AIのアイデアを比較し、採用理由を自分で決められる

題材の例

題材 保存する主なデータ 最初の2機能
学習記録 学んだ内容 記録する、一覧を見る
読書メモ 本の名前、感想 メモを作る、一覧を見る
買い物メモ 品名、個数 追加する、一覧を見る
お気に入りリンク タイトル、URL 登録する、一覧を見る
行きたい場所 場所、メモ 登録する、一覧を見る

最初の個人制作では、次の範囲を目安にします。

  • 主なテーブルは1つ。必要なら補助テーブルを1つまで
  • 画面は1つか2つ
  • APIは2つから4つ
  • 作成と一覧表示を必須にする
  • 更新または削除は、余裕がある場合だけ追加する
  • ログイン、決済、ファイル投稿、チャット、リアルタイム通信は発展課題にする
  • 本物の個人情報、医療情報、パスワードなどを保存しない

メンターに相談する目安

実装を始める前に、「誰が使うか」「最も大切な一つの操作」「今回は作らないもの」をメンターと確認してください。3週間で完成できるか判断できない機能は、まず任意機能へ移します。自分の案を先に仮決定し、最も迷っている点を一つ添えて相談すると、設計の判断材料を得やすくなります。

個人制作用のブランチを作る

第5週の設計を始める前に、未コミットの変更がないことを確認します。working tree cleanと表示されてから、個人制作用ブランチを作ります。

git status
git switch -c personal-project

ブランチは、同じ履歴から変更の流れを分ける仕組みです。mainには第4週の共通アプリを残し、personal-projectで設計と実装を進めます。新しいGitリポジトリを内側へ作ったり、node_modulesをコピーしたりしません。

セッション1:問題と利用者を決める

docs/product-brief.mdを作ります。

# プロダクト概要

## 誰が使うか

## どんな困りごとがあるか

## このアプリでできるようにすること

## 最も大切な一つの操作

## 今回は作らないもの

## 完成したと判断する条件

「便利なアプリを作る」だけでは、完成を判断できません。「学習者が、その日に学んだ内容を1件記録し、後から一覧で見られるようにする」のように、利用者と操作を確認できる目標にします。

AIに複数案を出してもらい、採用する案と開発範囲は自分で決めます。

8週間のWeb開発初学者コースで、残り3週間で作れる
個人制作案を5つ出してください。
条件はReact、Hono、PostgreSQLを使い、画面2つ以内、
主テーブル1つ、認証なし、作成と一覧だけです。
各案の難しい点も書いてください。まだコードは作らないでください。

セッション2:利用者の操作と完了条件

利用者の操作を、次の形で2〜3個書きます。

## ユーザーストーリー

- 利用者として、学習内容を記録したい。後から振り返るため。

### 完了条件

- 内容を1〜100文字で入力できる
- 保存成功時に一覧へ追加される
- 空白だけなら保存されない
- ページを再読み込みしても残る

完了条件は「実装した」ではなく、ブラウザやHTTPレスポンスで確認できる形にします。

セッション3:ワイヤーフレームを作成する

紙、ホワイトボード、またはdocs/wireframe.mdに画面を描きます。

+----------------------------------+
| 学習記録                         |
| [ 学んだ内容                  ]  |
| [ 保存 ]                         |
|                                  |
| 今日の記録                       |
| - TypeScriptの型                 |
| - ReactのProps                   |
+----------------------------------+

次の状態も描きます。

  • まだデータがない
  • 読み込み中
  • 保存に失敗した
  • 入力が正しくない

セッション4:APIとDBを対応させる

docs/design.mdに表を作ります。

## DB

| 列 | 型 | 条件 | 意味 |
|---|---|---|---|
| id | INTEGER | GENERATED ALWAYS AS IDENTITY PRIMARY KEY | DBが発行する保存番号 |
| content | VARCHAR(100) | NOT NULL、1〜100文字 | 学習内容 |

## API

| 操作 | メソッドとパス | リクエスト | 成功レスポンス |
|---|---|---|---|
| 一覧 | GET /api/records | なし | RecordItem[] |
| 作成 | POST /api/records | { content } | RecordItem |

画面のボタンがどのAPIを呼び、APIがどのSQLを実行するか、線で説明できるようにします。

セッション5:設計を記録し、実装の骨組みを確認する

現在のブランチを確認します。

git branch --show-current

personal-projectと表示されたら、第1〜4週と同じfrontendbackenddatabaseの骨組みが残っていることをlsで確認し、設計文書をコミットします。

ls
git add .
git commit -m "docs: define personal project MVP"

第5週の到達チェック

  • 「誰の何を解決するか」を30秒で説明できる
  • 必須機能が作成と一覧を中心に絞られている
  • 今回作らないものを明記した
  • データなし・読み込み中・失敗の各状態を考えた
  • DB列とAPIのリクエスト・レスポンスを表にした
  • 各機能に確認可能な完了条件がある
  • AIの提案を採用・却下した理由を記録した
  • personal-projectブランチで作業している
  • 設計をGitへコミットした

第6週:作成機能を画面からDBまで動かす

第6週は、1件を作成する操作をReact→Hono→PostgreSQLの順につなぎ、画面からDBまで一通り動作させます。一つの機能を端から端まで確認してから、次の機能へ進みます。

第6週の目標

  • 個人制作のCompose環境を起動できる
  • 1件を作成するAPIと一覧を読むAPIを作れる
  • Reactから作成と一覧取得を行える
  • 入力検証とパラメーター化SQLを使える
  • 機能単位の小さなコミットを残せる

第6週のセッション配分

セッション 作業 確認する結果
1 DBテーブルを作る psqlでテーブルを確認できる
2 GET APIを作る curlで一覧を取得できる
3 POST APIを作る curlで1件作成し、一覧でも確認できる
4 Reactから作成・一覧取得を行う ブラウザで保存と一覧表示ができる
5 不正入力・通信失敗・ビルドを確認する 結果を記録し、Gitへコミットできる

実装する順番

1. CREATE TABLEで保存場所を作る
2. GET APIを作り、curlで空の一覧を読む
3. POST APIを作り、curlで保存して一覧でも確認する
4. ReactのフォームからPOSTする
5. ReactでGETした一覧を表示する
6. 不正入力と通信失敗を確認する

APIを先にcurlで確認すると、画面の問題とサーバーの問題を分けられます。

個人制作のテーブルを作る

database/init.sqlを自分の設計へ変更します。学習記録アプリなら、最小のテーブルは次の形です。

CREATE TABLE IF NOT EXISTS records (
  id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  content VARCHAR(100) NOT NULL CHECK (btrim(content) <> '')
);
  • GENERATED ALWAYS AS IDENTITY: DBがIDを自動発行する
  • PRIMARY KEY: 各行を一意に識別する
  • VARCHAR(100): 100文字まで保存する
  • NOT NULLCHECK: 値なしと半角スペースだけの値を拒否する

第4週のVolumeはすでに存在するため、init.sqlを編集しただけでは自動実行されません。保存済みデータを消さず、変更後のSQLを明示的に実行します。新しいターミナルをプロジェクトルートで開いてください。

docker compose up -d db
docker compose ps
docker compose exec -T db psql -U app_user -d greeting_app \
  -v ON_ERROR_STOP=1 < database/init.sql

dbhealthyになってから最後のコマンドを実行します。-Tはファイルの内容を標準入力から渡せるようにする指定、ON_ERROR_STOP=1はSQLエラーで処理を止める指定です。通常はdocker compose down -vでVolumeを消す必要はありません。

この方法で確実にできるのは、例のrecordsのような新しいテーブルの作成です。CREATE TABLE IF NOT EXISTSは、同じ名前のテーブルがすでにあると何も変更しません。

既存テーブルの列や制約を変えるには、ALTER TABLEや、変更履歴をSQLファイルなどで順番に管理するマイグレーションが必要です。この8週間では、新しいテーブルを1つ作る範囲に絞ります。

メンターに相談する目安

既存テーブルの列や制約を変更したい場合や、Volumeの削除が必要だと考えた場合は、コマンドを実行する前にメンターへ相談してください。残したいデータ、現在のテーブル定義、変更したい内容を共有し、データを失わない手順を確認します。

GETを使うため、HonoのCORS設定も更新します。

allowMethods: ['GET', 'POST', 'OPTIONS'],

一覧APIの基本形

次の例を自分の設計へ合わせるときは、型名RecordItem、テーブル名records、列名content、APIパス/api/recordsをまとめて置き換えます。

type RecordItem = {
  id: number
  content: string
}

app.get('/api/records', async (c) => {
  const result = await pool.query<RecordItem>(
    'SELECT id, content FROM records ORDER BY id DESC',
  )

  return c.json(result.rows)
})

SQLはコードへ固定し、利用者入力を文字列連結しません。検索条件などの値が必要な場合も$1と値の配列を使います。

作成APIの基本形

第4週のPOST /api/greetingsと同じ順序で、項目名とSQLを個人制作向けに変えます。

app.post('/api/records', async (c) => {
  const body: unknown = await c.req.json().catch(() => null)

  if (
    typeof body !== 'object' ||
    body === null ||
    !('content' in body) ||
    typeof body.content !== 'string'
  ) {
    return c.json({ error: '内容を文字列で送ってください' }, 400)
  }

  const content = body.content.trim()

  if (content === '' || content.length > 100) {
    return c.json({ error: '内容は1〜100文字で入力してください' }, 400)
  }

  const result = await pool.query<RecordItem>(
    `INSERT INTO records (content)
     VALUES ($1)
     RETURNING id, content`,
    [content],
  )

  const createdRecord = result.rows[0]

  if (createdRecord === undefined) {
    throw new Error('作成した記録を取得できませんでした')
  }

  return c.json(createdRecord, 201)
})

bodyunknownから確認し、値を$1[content]へ分け、作成した1行を201で返します。ソースコードはコンテナへ自動反映されないため、まずプロジェクトルートからDBとバックエンドを再ビルドして起動します。

docker compose up --build db backend

dbhealthyになり、backendにAPI起動ログが表示されたら、別のターミナルをプロジェクトルートで開いてAPIだけを確認します。

curl -i -X POST http://localhost:3000/api/records \
  -H 'Content-Type: application/json' \
  -d '{"content":"TypeScriptの型を学んだ"}'
curl -i http://localhost:3000/api/records

POSTが201、GETが配列を含む200を返せば、DBとAPIまでつながっています。確認後は、起動ログを表示しているターミナルでCtrl + Cを押します。

Reactで作成と一覧をつなぐ最小形

次はfrontend/src/App.tsxの最小完成例です。型名RecordItem、State名recordscontent、APIパス/api/records、画面の見出しとラベルを、自分の設計へ合わせて置き換えます。

import { useEffect, useState } from 'react'
import type { FormEvent } from 'react'

type RecordItem = {
  id: number
  content: string
}

const API_URL = 'http://localhost:3000'

function App() {
  const [content, setContent] = useState('')
  const [records, setRecords] = useState<RecordItem[]>([])
  const [error, setError] = useState('')

  useEffect(() => {
    async function loadRecords(): Promise<void> {
      try {
        const response = await fetch(`${API_URL}/api/records`)

        if (!response.ok) {
          throw new Error(`API エラー: ${response.status}`)
        }

        const data = (await response.json()) as RecordItem[]
        setRecords(data)
      } catch {
        setError('一覧の読み込みに失敗しました')
      }
    }

    void loadRecords()
  }, [])

  async function handleSubmit(
    event: FormEvent<HTMLFormElement>,
  ): Promise<void> {
    event.preventDefault()

    const trimmedContent = content.trim()

    if (trimmedContent === '') {
      setError('内容を入力してください')
      return
    }

    setError('')

    try {
      const response = await fetch(`${API_URL}/api/records`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ content: trimmedContent }),
      })

      if (!response.ok) {
        throw new Error(`API エラー: ${response.status}`)
      }

      const createdRecord = (await response.json()) as RecordItem
      setRecords((currentRecords) => [createdRecord, ...currentRecords])
      setContent('')
    } catch {
      setError('記録の保存に失敗しました')
    }
  }

  return (
    <main>
      <h1>学習記録</h1>

      <form onSubmit={handleSubmit}>
        <label htmlFor="content">学んだ内容</label>
        <input
          id="content"
          value={content}
          maxLength={100}
          required
          onChange={(event) => setContent(event.target.value)}
        />
        <button type="submit">保存</button>
      </form>

      {error !== '' && <p>{error}</p>}

      {records.length === 0 ? (
        <p>まだ記録はありません。</p>
      ) : (
        <ul>
          {records.map((record) => (
            <li key={record.id}>{record.content}</li>
          ))}
        </ul>
      )}
    </main>
  )
}

export default App
  • useEffect(..., []): コンポーネントが最初に画面へ出た後に読み込む
  • useState<RecordItem[]>([]): 空配列へ要素の型を明示する
  • mapkey: 読み込んだ各行を、IDで区別して一覧表示する
  • setRecords((currentRecords) => ...): POSTが返した新しい行を、現在の一覧の先頭へ加える
  • [createdRecord, ...currentRecords]: 新しい配列を作るスプレッド構文

この例では、初回のGET結果をStateへ入れ、POST成功後は返ってきた1行を同じStateへ追加しています。そのため、保存直後にも一覧が更新されます。第7週で読み込み中、通信中、詳しいエラー表示を加えます。

ViteのReactテンプレートは、開発中にStrictModeを使います。その確認動作によりEffectが追加でもう一度実行され、NetworkタブにGETが2回見える場合があります。本番ビルドでは追加実行されません。GETは、何度実行されてもデータを変更しない処理として作ります。

個人制作の3サービスを起動する

バックエンドとフロントエンドの変更はDockerイメージへ自動反映されないため、プロジェクトルートから再ビルドして起動します。

docker compose up --build

ブラウザでhttp://localhost:5173を開き、保存直後とページ再読み込み後の両方で一覧を確認します。コードをさらに変更した場合はCtrl + Cで停止し、同じコマンドでもう一度ビルドします。

Gitの進め方

1機能ごとに、プロジェクトルートから次の流れを繰り返します。

git status
git diff
cd frontend
npm run build
cd ../backend
npm run build
cd ..
git add backend/src/index.ts
git commit -m "feat: add record creation API"

これはバックエンドの作成APIを記録する例です。フロントエンドの機能ならgit add frontend/src/App.tsxのように、実際に変更したパスへ置き換えます。

AIへ依頼するときも、1回の依頼を1機能へ限定します。

POST /api/recordsの実装だけを手伝ってください。
変更してよいのはbackend/src/index.tsだけです。
空白と100文字超は400、成功は201、SQLはパラメーター化します。
最初に現在のコードを読み、編集前に計画と確認方法を示してください。

第6週の到達チェック

  • Composeで個人制作の3サービスが起動する
  • curlで1件作成できる
  • curlで一覧を取得できる
  • ブラウザから追加した行が一覧へ表示される
  • ページ再読み込み後もDBから一覧が戻る
  • APIへ直接不正値を送っても拒否される
  • SQLへ利用者入力を直接連結していない
  • frontendbackendのビルドが成功する
  • AIが変更した中心コードを自分で説明できる
  • 第6週の変更をGitへコミットした

第7週:個人制作を完成させ、品質を整える

第7週の前半で必須機能を完成させ、後半は新機能を増やしません。エラー時の表示、他の人が起動できる説明、公開準備へ時間を使います。

第7週の目標

  • 読み込み中、データなし、失敗を区別して表示できる
  • フォームの二重送信や入力エラーを扱える
  • 基本的なアクセシビリティを確認できる
  • READMEだけで第三者が起動できる状態にできる
  • localhost固定値を本番用の環境変数へ分けられる
  • AIレビューを根拠付きで採否判断できる

セッション1〜2:必須機能を安定させる

まず3サービスをバックグラウンドで起動し、dbhealthyになるまで待ちます。

docker compose up --build -d
docker compose ps

次の状態をそれぞれ再現し、画面とログの結果を記録します。

状態 再現方法 期待する画面 確認後の操作
読み込み中 Networkタブで通信を一時的に低速化 読み込み中と表示 速度設定を元へ戻す
データなし 空のテーブルを読む まだ記録がないと表示 必要ならテストデータを戻す
入力エラー 空白・上限超過を入力 入力方法を表示 正常値でも確認する
通信失敗 backendを停止 読み込み・保存失敗を表示 backendを再開する
DB失敗 dbを停止 APIは500、画面は失敗表示 dbを再開し、healthyを待つ
成功 正常値を入力 一覧へ反映 再読み込み後も確認する

読み込み中と送信中を区別するため、frontend/src/App.tsxの他のStateの直後へ次を追加します。

const [isLoading, setIsLoading] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)

loadRecordsでは、既存のGET処理をtryに残したまま、最後に必ず読み込み状態を解除します。次は変更箇所を示す抜粋です。

async function loadRecords(): Promise<void> {
  setIsLoading(true)

  try {
    // 第6週で作ったfetchとsetRecordsの処理をここに残す
  } catch {
    setError('一覧の読み込みに失敗しました')
  } finally {
    setIsLoading(false)
  }
}

handleSubmitでは、送信開始時にtrue、終了時にfalseへ戻します。既存の入力検証を通過した後に、次の形を加えます。

if (isSubmitting) {
  return
}

setIsSubmitting(true)

try {
  // 第6週で作ったPOST処理をここに残す
} catch {
  setError('記録の保存に失敗しました')
} finally {
  setIsSubmitting(false)
}

ボタンと一覧部分では、Stateに応じて表示を切り替えます。

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? '保存中…' : '保存'}
</button>

{isLoading ? (
  <p>読み込み中…</p>
) : records.length === 0 ? (
  <p>まだ記録はありません。</p>
) : (
  <ul>{/* 第6週のmapによる一覧をここに残す */}</ul>
)}

finallyは成功・失敗のどちらでも最後に実行されます。disabledは送信中のボタン操作を無効にします。try / catch / finally詳細リファレンス第2章の2-11でも確認できます。

状態を再現する具体例です。コマンドはプロジェクトルートから実行します。

  • 読み込み中: ブラウザの開発者ツールでNetworkタブを開き、通信速度をSlow 3Gなどへ一時的に制限して再読み込みする。確認後はNo throttlingへ戻す
  • backend停止: docker compose stop backendの後に画面から操作し、確認後にdocker compose start backendで戻す
  • DB停止: backendは動かしたままdocker compose stop dbを実行して操作し、確認後にdocker compose start dbを実行する。docker compose psで再びhealthyになるまで待つ
  • データなし: 新しいテーブルへ最初の行を入れる前に確認する。すでに行がある場合、次の削除は学習データを消してよい場合だけ行う
docker compose exec db psql -U app_user -d greeting_app \
  -c 'DELETE FROM records;'

自分のテーブル名がrecordsでなければ置き換えます。このDELETEは対象テーブルの全行を削除し、元へ戻せません。残したいデータがある場合は実行せず、別の空テーブルやテスト用DBで確認します。

保存ボタンを通信中だけ無効にすると、二重送信を減らせます。ラベルと入力欄をhtmlForidで関連付け、キーボードだけでも操作できるか確認します。

セッション3:公開用設定へ分ける

フロントエンドのAPI URLを環境変数へ移します。

const API_URL =
  import.meta.env.VITE_API_URL ?? 'http://localhost:3000'

fetchは次のようにします。

fetch(`${API_URL}/api/records`)

VITE_で始まる値はビルド時にブラウザ用コードへ含まれます。公開APIのURLは置けますが、DBパスワード、APIキー、トークンは絶対に置きません。

バックエンドでは、CORS許可元とポートを環境変数へ移します。

const corsOrigin =
  process.env.CORS_ORIGIN ?? 'http://localhost:5173'

const port = Number(process.env.PORT ?? '3000')

if (!Number.isInteger(port) || port < 1 || port > 65535) {
  throw new Error('PORTが正しくありません')
}
cors({
  origin: corsOrigin,
  allowMethods: ['GET', 'POST', 'OPTIONS'],
  allowHeaders: ['Content-Type'],
})

完成コードのserveにあるport: 3000portに変更します。動作確認用のエンドポイントも加えます。

app.get('/health', (c) => c.json({ status: 'ok' }))

DATABASE_URLCORS_ORIGINPORTはバックエンドだけが読む設定です。.envをGitへコミットせず、必要な変数名だけを.env.exampleとREADMEへ記載します。

パスワードや接続URLのように、公開してはいけない設定値を**シークレット(Secret)**と呼びます。環境変数は値をコードの外へ出す手段ですが、それだけで秘密になるわけではありません。Git、ブラウザ用コード、ログ、AIへの入力へ出さず、公開先のシークレット管理機能へ設定します。

frontend/.env.example

VITE_API_URL=http://localhost:3000

backend/.env.example

DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DATABASE
CORS_ORIGIN=http://localhost:5173
PORT=3000

.env.exampleは必要な設定名を伝える見本であり、本物のパスワードを書かないためGitへ記録できます。実際の値を入れる.env.env.localはGitとDockerイメージから除外します。

セッション4:AIレビューと自分の検証

複数のAIを利用できる場合は、一方にレビューを依頼し、もう一方には「その指摘が誤りである可能性」を検討させます。2つのAIが同じ意見でも、正しさの証明にはなりません。

このプロジェクトを編集せずレビューしてください。
優先順は、秘密情報、入力検証、SQLインジェクション、CORS、
エラー処理、アクセシビリティ、初心者が説明できない複雑さです。
指摘ごとに根拠、再現手順、最小修正案を書いてください。

採用前に、該当コード、公式ドキュメント、再現結果を確認します。

セッション5:READMEとリリース候補

個人制作のREADMEへ次を含めます。

  • アプリが解決する問題
  • 画面のスクリーンショット
  • 画面・API・DBの構成図
  • 必要なソフトウェア
  • ローカル起動手順
  • 環境変数の名前と意味。実際の秘密値は書かない
  • 正常系と異常系の確認手順
  • 既知の制限

新しいターミナルをプロジェクトルートで開き、まず両方のビルドを確認します。

cd frontend
npm run build
cd ../backend
npm run build
cd ..

フロントエンドとバックエンドのビルドが成功したら、リリース候補をコミットします。

git add .
git commit -m "docs: prepare release candidate"

次に個人制作をmainへ取り込みます。

git switch main
git merge --no-ff personal-project -m "merge: complete personal project"

git switch mainで共通アプリから始めた本流へ戻り、git mergeで個人制作のコミットを取り込みます。

競合が表示された場合は、自動的に解決せず、次の順に確認します。

メンターに相談する目安

競合した変更のどちらを残すべきか説明できない場合は、推測で編集せず、git statusと競合したファイルをメンターへ見せてください。競合マーカーを削除するだけでは正しい解決になりません。パスワードなどが含まれていないことを確認してから画面や差分を共有します。

  1. git statusで競合しているファイルを確認する
  2. 対象ファイルをエディターで開き、両方の変更を読んで残す内容を決める
  3. git add frontend/src/App.tsxのように、解決したファイルをステージする
  4. すべて解決したことをgit statusで確認する
  5. 次のコマンドでマージコミットを作る
git commit -m "merge: complete personal project"

解決を中止し、マージ前の状態へ戻す場合だけgit merge --abortを使います。マージが完了したら状態を確認し、working tree cleanの場合だけタグを付けます。

git status
git tag v1.0.0-rc.1

rcはRelease Candidateの略で、正式公開前の確認版を表します。第8週で公開確認を終えた後、正式版のv1.0.0タグを付けます。

第8週の事前準備(授業時間外)

アカウント作成や認証に必要な時間は一定ではありません。次の準備は、第6週終了後から第7週終了までの授業時間外に完了してください。

GitHubは、Gitで管理したリポジトリをインターネット上へ置き、共有できるサービスです。Gitという履歴管理の仕組みそのものとは別です。

  1. GitHubへサインインする
  2. GitHub公式の作成手順を参照し、READMEやライセンスを自動追加しない空のリポジトリを作る
  3. 下のURLを自分のリポジトリURLへ置き換えて、コードを送る
  4. Renderへサインインし、GitHub連携が可能か確認する
  5. RenderのFreeプラン、表示料金、利用制限を確認する。まだサービスやDBは作らない
git remote add origin https://github.com/your-name/your-repository.git
git push -u origin main --tags

GitHubのページでREADMEとコミット履歴が見えれば成功です。

認証できない場合は、GitHubの認証に関する公式案内とエラーメッセージを確認し、第8週より前に解決します。

originは送信先リポジトリへ付ける慣例的な名前です。pushはローカルのコミットとタグをGitHubへ送ります。

第7週の到達チェック

  • 必須機能に未完成箇所がない
  • 読み込み中、データなし、入力エラー、通信失敗を区別した
  • ラベルとキーボード操作を確認した
  • API URL、CORS許可元、ポートを環境別に設定できる
  • .envや秘密情報をコミットしていない
  • READMEだけで第三者が起動できる
  • frontendbackendのビルドが成功する
  • AIレビューの採用・却下理由を記録した
  • リリース候補タグを付けた
  • personal-projectmainへマージした
  • GitHubへmainとタグをpushできた
  • Renderへサインインし、作成前の料金確認箇所が分かる

第8週:デプロイし、成果を説明して振り返る

デプロイは、自分のPCで動いていたアプリを、他の人がアクセスできる環境へ配置することです。

第8週の目標

  • ローカル開発環境と公開環境の違いを説明できる
  • 公開してよい環境変数とシークレットを区別できる
  • DB、バックエンド、フロントエンドの順にデプロイ・確認できる
  • ログを見て失敗した場所を切り分けられる
  • 制作物の目的、構成、工夫、課題を自分の言葉で発表できる

この教材の開発用Composeをそのまま公開するのではなく、3つの役割を別々のサービスへ配置します。

アプリ内の役割 Renderで使うサービス
Reactのビルド結果 Static Site
Node.jsで動くHono Web Service
データを保存するPostgreSQL Render Postgres

開発環境と本番環境の違いです。

開発中 デプロイ先
Vite開発サーバー npm run buildで作った静的ファイル
tsx watch / npm run dev TypeScriptをビルドし、Node.jsで起動
Compose内のPostgreSQL マネージドPostgreSQL
名前付きVolume 事業者が管理するDB保存領域。バックアップの有無はプランごとに確認
init.sqlの初回マウント SQLまたはマイグレーションを明示的に実行
Composeに書いた学習用パスワード 管理画面のシークレット
localhostのURL HTTPSの公開URL

Dockerイメージを使っても、設定、シークレット、DBの保存方式まで自動的に本番向けになるわけではありません。Volumeもバックアップそのものではありません。

デプロイ先について

実習例には、Webアプリを公開するサービスであるRenderを使います。Render以外を使う場合も、「静的フロントエンド + Node.jsバックエンド + マネージドPostgreSQL」という同じ役割へ対応させます。

マネージドDBは、DBサーバーの基本的な運用をサービス事業者が管理する方式です。ただし、保存期限やバックアップの有無はサービスとプランによって異なります。

提供プラン、料金、停止条件、DB保持期間は変わります。作成前に次の公式資料を確認してください。

2026年8月時点のFreeプランには、主に次の制限があります。

項目 制限・注意
Web Service 15分間アクセスがないと停止し、次のアクセスからの復帰に約1分かかる場合がある
PostgreSQL 1GBまで。作成から30日で期限切れ。バックアップ機能なし
Workspace Free Postgresは1つ。Free Web Serviceの実行時間は全体で月750時間
帯域・ビルド 月間枠があり、超過時の扱いは支払い方法の登録状況によって異なる

料金とデータに関する注意: 作成画面でFreeと表示されていること、表示料金、Workspaceの使用量、支払い方法の有無を確認してから確定します。支払い方法がある場合は、利用枠の超過で追加請求が発生する可能性があります。Freeプランは学習・試用向けであり、消失して困るデータには使用しません。

メンターに相談する目安

作成画面にFree以外の料金が表示される、費用の発生条件を判断できない、または公開範囲に迷う場合は、確定する前にメンターへ相談してください。画面を共有するときは、個人情報やシークレットが写っていないことを確認します。接続URL、APIキー、トークンを公開した可能性がある場合は公開作業を止め、すぐにメンターへ伝えてください。

セッション1:公開前の状態を確認する

第8週の事前準備で作成したGitHubリポジトリを開きます。送信前に秘密がないことをもう一度確認し、最新コミットとタグを送ります。

git status
git log --oneline
git remote -v
git push origin main --tags

公開リポジトリへする場合は、コード、履歴、READMEを誰でも読めると考えてください。過去のコミットに秘密を入れた場合、最新ファイルから消すだけでは不十分です。その秘密を無効化・再発行してから履歴の対処を行います。

セッション2:PostgreSQLを用意する

Render DashboardでPostgreSQLを作成します。

  1. 前節で確認したプランを選ぶ
  2. 選んだWorkspaceとリージョンを記録する
  3. スキーマの初期化には、自分のPCから接続するためのExternal URLを使う
  4. 次のセッションで、バックエンドを同じWorkspace・同じリージョンへ作る
  5. バックエンドのDATABASE_URLにはInternal URLを設定する

開発用の/docker-entrypoint-initdb.dはマネージドDBでは動きません。Renderの接続画面には、Render内のサービスから使うInternal URLと、自分のPCから接続するときに使うExternal URLがあります。External URLはパスワードを含む秘密です。

ここでは、PCへpsqlを直接インストールせず、PostgreSQLのDockerイメージに入っているpsqlからdatabase/init.sqlを一度実行します。第1週の.gitignoreでは.env.*を除外済みです。プロジェクトルートに.env.render.localを作ります。

RENDER_DATABASE_URL=Renderに表示された完全なExternal URL

プロジェクトルートで実行します。

docker run --rm -i \
  --env-file .env.render.local \
  -v "$PWD/database/init.sql:/init.sql:ro" \
  postgres:18-alpine \
  sh -c 'psql "$RENDER_DATABASE_URL" -v ON_ERROR_STOP=1 -f /init.sql'
  • --rm: SQL実行後に一時コンテナを削除する
  • --env-file: Git管理外のファイルから接続URLを一時コンテナへ渡す
  • $PWD: 現在いるプロジェクトルートの絶対パス
  • -v ...:/init.sql:ro: PCのSQLファイルを、読み取り専用でコンテナへ見せる
  • psql ... -f /init.sql: 指定したDBへ接続し、SQLファイルを実行する

CREATE TABLEが成功したら、RenderのNetworking設定で外部アクセスを無効化するか、自分のIPアドレスだけに制限します。今後スキーマを変更するときだけ、必要な時間に再び許可します。.env.render.localはGitへ追加せず、画面共有やAIへの入力にも含めません。

学習用Compose: 空のVolumeならinit.sqlを自動実行
デプロイ先DB:  接続してSQLまたはマイグレーションを実行

DBの接続URLをREADME、スクリーンショット、AIへの入力へ貼り付けません。Free Postgresにはバックアップがないため、消したくないデータを入れず、必要なら自分でエクスポートします。

セッション3:Honoをデプロイする

RenderでWeb Serviceを作り、GitHubリポジトリを接続します。この教材のように1つのリポジトリへfrontendbackendを置く構成をモノレポと呼びます。

サービスの起点となるフォルダーを、Root Directoryで指定します。バックエンドのRoot Directoryはbackendです。

設定例です。実際のpackage.jsonのscriptsも確認してください。

項目 値の例
Runtime Node
Root Directory backend
Build Command npm ci && npm run build
Start Command npm start
Health Check Path /health
DATABASE_URL Render Postgresの内部接続URL
CORS_ORIGIN 初回は設定せずlocalhostの既定値を使う。フロントエンド公開後にそのURLを設定

RenderのWeb Serviceは0.0.0.0で待ち受け、環境変数PORTのポートを使う必要があります。第7週の設定が反映されていることを確認します。

デプロイ後、公開APIを単独で確認します。

curl -i https://your-backend-name.onrender.com/health

your-backend-nameの部分は、Renderに表示された自分のバックエンド名へ置き換えます。

次に、公開URLへ正常値と空白を送ります。

curl -i -X POST https://your-backend-name.onrender.com/api/records \
  -H 'Content-Type: application/json' \
  -d '{"content":"デプロイを確認した"}'

curl -i -X POST https://your-backend-name.onrender.com/api/records \
  -H 'Content-Type: application/json' \
  -d '{"content":"   "}'

個人制作でAPIパスや項目名を変更した場合は、/api/recordscontentも自分の設計へ合わせます。正常値が201、空白が400を返すことを確認します。失敗した場合は、最初にRenderのバックエンドログを読みます。

セッション4:Reactをデプロイする

RenderでStatic Siteを作ります。前節で確認したプランを選び、Viteの静的デプロイ手順も参照してください。

項目
Root Directory frontend
Build Command npm ci && npm run build
Publish Directory dist
VITE_API_URL Honoの公開URL。末尾の/api/...は付けない

フロントエンドのURLが決まったら、バックエンドのCORS_ORIGINをその正確なURLへ設定して再デプロイします。httphttps、末尾、サブドメインの違いに注意します。

公開画面から1件保存し、次の3か所を確認します。

  1. ブラウザのNetworkタブでAPIが201を返す
  2. バックエンドログに予期しないエラーがない
  3. PostgreSQLで保存した行を確認できる

バックエンドを再デプロイしてもDBの行が残ることも確認します。

この教材のアプリにはログイン機能がないため、公開URLを知る人はAPIへ保存リクエストを送れます。本物の個人情報や秘密を入力せず、発表と確認が終わったら不要なサービスとDBを停止・削除してください。削除前に、残す必要のある学習記録をエクスポートします。

セッション5:発表と振り返り

5分のデモを次の順で準備します。

  1. 誰のどんな問題を解決するか
  2. 実際の操作
  3. React、Hono、PostgreSQLのデータの流れ
  4. 一番難しかった問題と、調べて直した方法
  5. AIの提案を採用しなかった例、またはAIの誤りを検証した例
  6. 次に改善したいこと

docs/retrospective.mdを作ります。

# 8週間の振り返り

## 作れるようになったこと

## 自分の言葉で説明できる技術

## 一番時間がかかった問題と解決過程

## AIが役立った場面

## AIの提案を疑って確認した場面

## スコープを小さくして良かったこと

## もう一度作るなら変えること

## 次の4週間で学ぶこと

振り返りをコミットし、正式版タグv1.0.0をGitHubへ送ります。

新しいターミナルをプロジェクトルートで開いて実行します。

git add .
git commit -m "docs: add deployment and course retrospective"
git tag v1.0.0
git push origin main --tags

第8週の到達チェック

  • HTTPSの公開画面から主要操作ができる
  • API URL、CORS、PORTが環境変数化されている
  • DB認証情報がフロントエンド、Git、AIログにない
  • 本番DBへテーブルを作る手順を記録した
  • 再デプロイ後もDBデータが残る
  • frontendbackend、DBのログ・状態を確認できる
  • 料金・保持期間・不要時の削除方法を確認した
  • 5分のデモを行った
  • 振り返りを自分の言葉で書いた
  • v1.0.0タグを作成した

第II部:第1〜4週の詳細リファレンス

ここからは、第4週で完成する共通アプリのコードと設定を詳しく説明します。先頭から順番に通読する章ではありません。各週の「読む場所」で指定された節や、実装中に確認したい節を参照してください。

第2週・第3週の途中版ではなく、第4週終了時点の完成コードを掲載しています。

目的 参照する章
TypeScriptの構文を確認する 第2章
DBとDockerの考え方を確認する 第3章
共通アプリを最短経路で作る 第4〜12章、続けて第14章
エラーの原因を調べる 第13章
小さな変更を試す 第15章
全体を振り返る 第16章

2. TypeScriptの概念と基本文法

この章は、実装中にTypeScriptの構文を確認するための参照資料です。章全体を一度に読む必要はありません。

時期 主に読む節 理由
第1週 2-1、2-2、2-8 JavaScriptとTypeScript、値、関数の入口
第2週 2-3〜2-9、2-12、2-13 ReactのState、Props、イベントを読む準備
第3週 2-10〜2-14 API入力、非同期通信、レスポンス型
第4週 2-5、2-6、2-10、2-11、2-13、2-14 環境変数、DB問い合わせ、型と実データの境界

表で指定されていない節は、必要になった時点で参照します。

ReactやNode.js固有の型を使わない短い例は、TypeScript Playgroundで確認できます。別のコードブロックに同じ変数名があるため、例は原則として1つずつ貼り付けてください。

process.envpool、Reactの型を含む例は完成コードからの抜粋であり、単独実行用ではありません。

2-1. TypeScriptとJavaScriptの関係

TypeScriptは、JavaScriptに型を記述・検査する仕組みを加えたプログラミング言語です。

TypeScript(.ts / .tsx)
    ├─ エディター / tsc ─→ 型を検査
    └─ Vite / tsx ─────→ 型を取り除き、実行可能な形へ変換
                              ↓
                       ブラウザ / Node.jsで実行

tscは設定によって型検査だけでなくJavaScriptへの変換もできます。この教材では、型検査と開発サーバーでの変換を役割として分けて示しています。

stringGreetingResponseなどの型情報は、実行用コードでは取り除かれます。型は開発中の間違いを見つけるために使われます。

npm run devでは、Viteやtsxが素早くコードを動かすため、型エラーがあってもサーバーが起動する場合があります。最後にnpm run buildを実行して型検査を通します。

  • .ts: 通常のTypeScriptファイル
  • .tsx: JSXを含められるTypeScriptファイル

constif、配列、関数、async / awaitなどはJavaScriptと共通の文法です。型注釈、type、Union型、unknownの型絞り込みなどがTypeScriptの型システムに関係する部分です。

TypeScriptはすべての実行時エラーを防ぐものではありません。利用者の入力、APIのJSON、DBの実際の列は、型とは別に確認する必要があります。

2-2. 値と基本型

はプログラムが実際に扱うデータです。は、その値をどのように扱えるかを表します。

const userName = 'Taro'
const savedId = 1
const isSaved = true
値の例 TypeScriptの型 意味
'Taro' string 文字列
1 number 数値
true boolean 真偽値
null null 明示的な「値なし」によく使う
undefined undefined 未代入や存在しない値などで現れる

今回の主な型対応です。

PostgreSQL TypeScript 今回の値
INTEGER number 保存番号id
VARCHAR(100) string 100文字までの名前name

TypeScriptの型とPostgreSQLの型は別の仕組みです。この対応は開発者がコードとテーブル定義の両方で保ちます。

2-3. 型推論と型注釈

TypeScriptは、代入した値から型を推論できます。

let language = 'TypeScript'
// language は string と推論される

変数名の後ろに: 型を書くと、型を明示できます。これを型注釈と呼びます。

const appName: string = 'あいさつ保存アプリ'
const firstId: number = 1
const isReady: boolean = true

違う型の値は代入できません。

let id: number = 1

// エラー: string は number に代入できない
// id = 'one'

型が明らかな場所では推論を使い、関数の引数やデータの形など、約束を明確にしたい場所では型を記述します。

2-4. constlet

const databaseName = 'greeting_app'

let saveCount = 0
saveCount = saveCount + 1
  • const: 変数へ別の値を再代入しない
  • let: 後から別の値を再代入できる

基本はconstを使い、再代入が必要な場合だけletを使います。

constはオブジェクトの中身まで変更不能にするものではありません。

const greeting = { id: 1, name: 'Taro' }
greeting.name = 'Hanako'

2-5. 演算子と条件分岐

演算子 意味
= 代入 name = 'Taro'
=== 等しい name === 'Taro'
!== 等しくない name !== ''
&& かつ isReady && isValid
|| または name === '' || id < 1
! 否定 !response.ok

ifは、条件が真として扱われる場合だけ処理を実行します。この教材では比較結果などのbooleanを条件に使います。

const personName = 'Taro'

if (personName === '') {
  console.log('名前が空です')
} else {
  console.log('名前が入力されています')
}

完成コードでは環境変数の確認にもifを使います。

環境変数は、接続先やポートなどの設定を、ソースコードの外側からプログラムへ渡す仕組みです。第4週ではComposeからバックエンドへDATABASE_URLを渡します。

const databaseUrl = process.env.DATABASE_URL

if (databaseUrl === undefined) {
  throw new Error('DATABASE_URL が設定されていません')
}

環境変数は存在しない可能性があるため、databaseUrlは最初string | undefinedです。ifの後ではundefinedではないと分かり、stringとして扱えます。

2-6. オブジェクトとtype

オブジェクトは、複数の値をプロパティ名でまとめたものです。

const plainGreeting = {
  id: 1,
  name: 'Taro',
}

typeを使うと、オブジェクトの形に名前を付けられます。

type SavedGreeting = {
  id: number
  name: string
}

const greeting: SavedGreeting = {
  id: 1,
  name: 'Taro',
}

SavedGreetingは値ではなく型です。idがなかったり、nameが文字列でなかったりすると型エラーになります。

オブジェクトの値は、.を使って読み取ります。

console.log(greeting.name)
  • object.property: オブジェクトのプロパティを読む
  • object.method(): オブジェクトが持つメソッドを呼び出す

完成コードのbody.namebody.name.trim()pool.query(...)も同じ読み方です。

newは、クラスやコンストラクターから新しい実体(インスタンス)を作る構文です。

const pool = new Pool({ connectionString: databaseUrl })

この教材ではnew Hono()new Pool(...)new Error(...)を使います。クラスを自分で作る方法は発展学習へ回します。

フロントエンドが受け取るJSONにも型を付けます。

type GreetingResponse = {
  id: number
  message: string
}

2-7. 配列、コールバック関数、分割代入

配列は複数の値を順番に持つデータです。型[]で要素の型を表します。

const names: string[] = ['Taro', 'Hanako', 'Alex']

mapは各要素を別の値へ変換し、新しい配列を返します。

const names: string[] = ['Taro', 'Hanako', 'Alex']

const messages = names.map((name) => {
  return `こんにちは、${name}さん!`
})

(name) => { ... }アロー関数です。mapが後から呼び出すため、コールバック関数でもあります。

今回の最小アプリでは一覧を表示しないためmapは使いません。履歴一覧を追加するときによく使います。

配列から要素を取り出す書き方を配列の分割代入と呼びます。

const colors = ['red', 'blue']
const [firstColor, secondColor] = colors

完成コードのconst [name, setName] = useState('')も同じ文法です。useStateが返す「現在の値」と「更新関数」を二つの変数へ取り出します。

2-8. 関数、引数、戻り値

function createGreeting(name: string): string {
  return `こんにちは、${name}さん!`
}

const message = createGreeting('Taro')
  • name: string: 引数nameは文字列
  • : string: 戻り値は文字列
  • return: 呼び出し元へ値を返す

戻り値がない非同期関数はPromise<void>と書けます。

async function save(): Promise<void> {
  console.log('保存処理')
}

2-9. テンプレートリテラル

バッククォートで囲んだ文字列をテンプレートリテラルと呼びます。

const personName = 'Taro'
const message = `こんにちは、${personName}さん!`

${}の中へ変数や式を埋め込めます。

const id = 1
const label = `保存番号 ${id}`

TypeScriptのテンプレートリテラルと、後で登場するSQLの$1は別の仕組みです。

2-10. unknownと型の絞り込み

unknownは「まだ何型か分からない値」を表します。

const value: unknown = 'hello'

// string か未確認なのでエラー
// value.trim()

typeofなどで確認すると、その範囲では型が絞り込まれます。

const value: unknown = 'hello'

if (typeof value === 'string') {
  console.log(value.trim())
}

今回の入力検証を関数にまとめると、次のように書けます。完成コードでは同じ判定をHonoのルート内へ直接書きます。

function readName(body: unknown): string | null {
  if (
    typeof body !== 'object' ||
    body === null ||
    !('name' in body) ||
    typeof body.name !== 'string'
  ) {
    return null
  }

  const name = body.name.trim()

  if (name === '' || name.length > 100) {
    return null
  }

  return name
}

string | nullは「文字列またはnull」を表す Union型です。

JavaScriptではtypeof null'object'になるため、body === nullを別に確認します。'name' in bodyは、nameプロパティがあるか調べるin演算子です。

ネットワークから届くJSONはTypeScriptの外側から来ます。最初から正しい型と決めず、実行時に確認します。

2-11. Promise、async / await、エラー処理

HTTP通信やDB通信は、結果がすぐ返るとは限りません。将来完了する処理をPromiseで表します。

async function sendRequest(): Promise<void> {
  const response = await fetch('/api/example')
  console.log(response.status)
}
  • async: 非同期関数を表す
  • Promise<void>: 将来完了するが、値は返さない
  • await: Promiseが完了するまで、その関数内の続きを待つ

通信は失敗する可能性があるため、try / catchを使います。

async function sendRequestSafely(): Promise<void> {
  try {
    const response = await fetch('/api/example')

    if (!response.ok) {
      throw new Error('API エラー')
    }
  } catch {
    console.log('保存に失敗しました')
  } finally {
    console.log('通信処理が終了しました')
  }
}
  • try: 失敗する可能性がある処理
  • throw new Error(...): エラーを作って発生させる
  • catch: 発生したエラーを受けて処理する
  • finally: 成功・失敗のどちらでも最後に実行する

pool.query(...)もPromiseを返すため、HonoではawaitしてDBの結果を待ちます。

2-12. importexport

ファイルを分けた場合は、exportで外部へ公開し、importで読み込みます。次は二つのファイルを表す説明用の例です。

// greeting.ts
export function createGreeting(name: string): string {
  return `こんにちは、${name}さん!`
}
// main.ts
import { createGreeting } from './greeting'

console.log(createGreeting('Taro'))

型としてだけ使う名前はimport typeで読み込みます。

import type { FormEvent } from 'react'

Poolは実行時にDB接続を作る値なので、通常のimportです。

import { Pool } from 'pg'

2-13. ジェネリクスの見方

完成コードには型<別の型>という記法が登場します。

FormEvent<HTMLFormElement>

これは「HTMLのform要素で発生したFormEvent」を表します。

DB問い合わせにもジェネリクスを使います。次は、第8章の保存処理から抜粋した例です。

const result = await pool.query<SavedGreeting>(
  `INSERT INTO greetings (name)
   VALUES ($1)
   RETURNING id, name`,
  [name],
)

pool.query<SavedGreeting>は「結果の1行をSavedGreetingとして扱う」とTypeScriptに伝えます。

ジェネリクスを書いても、実際のPostgreSQLテーブルを検証するわけではありません。SQLの列とTypeScriptの型が一致するよう、開発者が両方を管理します。

2-14. 型アサーションと実行時検証

Reactの完成コードでは、APIのJSONを次のように読みます。

const data = (await response.json()) as GreetingResponse

as GreetingResponseは値を検証する処理ではありません。「この値をGreetingResponseとして扱う」とTypeScriptへ伝える型アサーションです。

型アサーション = TypeScriptへ型を伝える。実行時の確認はしない
実行時検証     = 実際の値を if や typeof で確認する
DB制約         = DBへ不正な行が入ることを防ぐ最後の条件

この教材では三段階を使います。

  1. Reactの型で、コードを書くときの間違いを減らす
  2. Honoで、ネットワークから届いたJSONを実行時に検証する
  3. PostgreSQLのNOT NULLCHECKで、保存する行にも制約を付ける

GreetingResponseはフロントエンド側だけの約束です。Honoの返却型と自動的に共有されるわけではありません。pool.query<SavedGreeting>もDBスキーマを自動確認しません。

今回は自作APIなので、Reactのレスポンス読み取りは型アサーションで簡略化します。外部APIや重要なシステムでは、レスポンスの実行時検証やスキーマ共有も検討します。

2-15. 完成アプリとの対応

文法・概念 完成アプリでの使用場所
type GreetingResponseResultMessagePropsSavedGreeting
型推論 useState('')の文字列State
配列の分割代入 [name, setName]
Props AppからResultMessageへ渡すmessage
条件付きレンダリング messageが空文字でない場合だけ子を表示
Union型と絞り込み DATABASE_URLの存在確認
unknown Honoが受け取るJSON
テンプレートリテラル 挨拶とエラーメッセージ
async / await fetchとDB問い合わせ
try / catch Reactの通信失敗処理
ジェネリクス FormEvent<...>pool.query<...>
型アサーション Reactのレスポンス読み取り

2-16. 文法のミニ演習

名前と保存番号を受け取り、表示用の文章を返す関数を完成させてください。

次のコードは未完成なので、最初は「戻り値がない」という型エラーになります。returnを実装するとエラーが消えることも確認してください。

function createLabel(id: number, name: string): string {
  // ここを実装する
}

console.log(createLabel(1, 'Taro'))

期待する結果です。

保存番号 1: Taro
解答例
function createLabel(id: number, name: string): string {
  return `保存番号 ${id}: ${name}`
}

この章で扱わないもの

最小アプリに必要ないため、次は発展学習へ回します。

  • クラス、継承、抽象クラス
  • 高度なジェネリクス
  • 複雑な型演算
  • APIスキーマの自動生成
  • ORM

3. データベースとDockerの基礎

第3週では、バックエンドを再起動するとメモリ上のデータが消えました。第4週は、まずデータを残す仕組みを理解し、その環境をDockerで起動します。

3-1. なぜデータベースが必要か

変数や配列の値は、通常、そのプログラムが動いている間だけメモリにあります。

バックエンド起動
  ↓
配列へ名前を追加
  ↓
バックエンド停止
  ↓
メモリの配列は消える

後からもう一度読みたいデータは、プログラムの外に保存する必要があります。この教材ではデータベースを使います。

  • DB: 保存されたデータと、その構造
  • DBMS: DBを作成・検索・更新する管理ソフトウェア
  • PostgreSQL: 今回使うDBMSの製品名

3-2. テーブル、行、列

PostgreSQLは、データを表の形で整理できます。

データベース greeting_app
└── テーブル greetings
    ├── 行: id=1, name=Taro
    └── 行: id=2, name=Hanako
用語 意味 今回の例
データベース テーブルなどをまとめる単位 greeting_app
テーブル 同じ構造の行を、列の定義に従って保存する単位 greetings
1件のデータが持つ項目 idname
保存された1件のデータ id=1, name=Taro
主キー 各行を一意に識別する列 id
制約 保存してよい値の条件 名前を必須・100文字までにする

テーブル、列、制約など、DBの構造をまとめてスキーマと呼びます。idはPostgreSQLが一意の番号を自動発行します。連続して見えても、失敗した処理などにより番号が飛ぶことはあります。

3-3. SQLはDBMSへの命令

SQLは、PostgreSQLなどのDBMSへ命令を書く言語です。第4週の共通アプリでは3つだけ使います。

SQL 意味
CREATE TABLE テーブルを作る
INSERT 新しい行を保存する
SELECT 保存した行を読む
INSERT INTO greetings (name)
VALUES ('Taro');

CREATE TABLECREATEはテーブルそのものを作る命令です。

発展:CRUDという分類

アプリの機能をCreate、Read、Update、Deleteに分類する考え方をCRUDと呼びます。この分類における行の作成はINSERTです。更新のUPDATEと削除のDELETEは、個人制作で必要になった場合に学びます。

3-4. ブラウザからDBへ直接接続しない

ブラウザへ配信されたフロントエンドコードは、利用者が確認できる場所で実行されます。DBの接続URLやパスワードをReactのコードへ書いてはいけません。

React ──HTTP/JSON──> Hono ──SQL/DB接続──> PostgreSQL

Honoが入力を検証し、DB接続情報をバックエンドだけに持たせます。CORSだけでDBやAPIの権限を守れるわけではありません。

3-5. なぜDockerを使うか

第4週は、Vite、Hono、PostgreSQLという複数のプログラムを同時に動かします。Dockerを使うと、必要な環境と起動方法をファイルへ記録し、同じ手順で再現しやすくなります。

最初に6つの用語を区別します。

Dockerfile ──build──> イメージ ──run──> コンテナ
                                      ↑
compose.yaml ──複数サービスをまとめる─┘

Volume ── コンテナとは別にDBデータファイルを残す
  • Dockerfile: イメージを作る手順書
  • イメージ: コンテナを作る起動用のひな形
  • コンテナ: イメージから実際に起動した、隔離されたプロセス
  • Docker Compose: 複数のコンテナ設定を一つのYAMLファイルで管理する仕組み
  • サービス: Composeで管理する1単位。今回はfrontendbackenddb
  • Volume: コンテナとは別にデータを残す保存領域

コンテナは完全な別PCではなく、ホスト上で隔離して動くプロセスです。VolumeはPostgreSQLそのものではなく、PostgreSQLが書くDBデータファイルの置き場所です。

この教材のDockerfileは、学習用の開発構成です。第8週の本番公開では、開発サーバーやDBの保存方式を公開環境向けに分けます。

3-6. Dockerfileからコンテナまで

Dockerfileは「どの土台を使い、何をコピーし、どのコマンドで起動するか」を書くファイルです。

node:24-alpine
  + package.json
  + npm ciで入れたパッケージ
  + TypeScriptのソースコード
  = この教材のアプリ用イメージ

同じDockerfileとロックファイルからイメージを作ることで、PCへ直接すべてを入れる場合より、環境差を小さくできます。ロックファイルは、この教材ではpackage-lock.jsonを指し、導入するパッケージの正確なバージョンを記録します。

compose.yamlは、どのイメージを使うか、どのポートを公開するか、どのVolumeを使うかなどを3サービス分まとめます。

3-7. コンテナ間の通信

Composeはサービスを同じデフォルトネットワークへ接続します。コンテナ間では、サービス名を通信先の名前として使えます。

backendコンテナ ── postgresql://...@db:5432/... ──> dbコンテナ

一方、ReactのJavaScriptはホスト上のブラウザで動きます。ブラウザはCompose内部のbackendという名前を知りません。

ブラウザ ── http://localhost:3000 ──> ホストの3000番
                                          ↓ ポート転送
                                     backendコンテナ

接続元によって使う名前が違います。

接続 使用するホスト名
ブラウザ → Hono localhost
Honoコンテナ → PostgreSQL db

コンテナ内のlocalhostは、そのコンテナ自身です。backendからlocalhost:5432へ接続してもdbには届きません。

  • ポートマッピング: ホスト側のポートをコンテナ内のポートへ転送する設定
  • 環境変数: 接続先などをコードの外から渡す値
  • healthcheck: コンテナ内のサービスが接続可能になったか確かめる処理

これらは第7章でcompose.yamlの実物と一緒に確認します。


4. 開発環境とプロジェクトを準備する

4-1. 必要なソフトウェア

  • Node.jsのサポート中のLTS版
  • Docker Desktop、またはDocker EngineとCompose
  • Git
  • VS Codeなどのエディター
  • ブラウザ

2026年8月の執筆時点ではNode.js 24 LTSとPostgreSQL 18を使用します。将来読む場合は、公式にサポートされているバージョンを確認してください。

  • LTS: Long Term Supportの略。長期間サポートされる安定版
  • Node.jsをインストールすると、npmも一緒に利用できる
  • PostgreSQLをPCへ直接インストールする必要はない。第4週にDockerがイメージを取得する
  • macOSやWindowsでは、通常はDocker Desktopを使う
  • Docker daemonは、イメージやコンテナを管理するバックグラウンドプログラム

操作する場所を整理します。

操作環境 すること
エディター 指定されたファイルへコードを書く
ターミナル フォルダー移動、パッケージ追加、アプリ起動を行う
ブラウザ 作った画面を開き、入力や通信を確認する

bashのコードブロックはターミナルへ一行ずつ入力します。tstsxsqlyamldockerfileは指定されたファイルへ書きます。textjsonhttpは構造や結果の例であり、指示がない限り入力しません。

起動コマンドは、サーバーを待ち受けるため終了せずに動き続けることがあります。止めるときはCtrl + C、別の操作をするときは新しいターミナルを開きます。

Docker Desktopを起動してから確認します。

node -v
npm -v
docker --version
docker compose version
docker info

docker composeは、docker-composeではなく間に空白を入れるCompose V2の形式です。

docker infoでServerの情報まで表示されれば、Docker daemonも起動しています。

command not foundと表示された場合は、対象ソフトウェアのインストールを確認し、ターミナルを一度閉じて開き直します。

この教材のコマンド例はbash / zshを前提にしています。Windowsでは第1週から同じWSLターミナルを使い、途中でPowerShellやGit Bashへ切り替えません。行末の\もWSLのbashで入力します。

4-2. プロジェクトフォルダーを作る

8週間ルートでは、第1週にgreeting-db-appを作成済みです。その場合は作り直しません。現在その親フォルダーにいる場合だけcd greeting-db-appを実行し、すでにプロジェクトルートにいる場合はpwdlsだけ実行します。

cd greeting-db-app
pwd
ls

この詳細リファレンスだけを単独で進める場合は、次のコマンドで新規作成します。

mkdir greeting-db-app
cd greeting-db-app
  • mkdir: 新しいフォルダーを作る
  • cd: 指定したフォルダーへ移動する

以降、特に記載がないコマンドはgreeting-db-app直下で実行します。

4-3. React + Viteを作る

8週間ルートでは第2週にfrontendを作成済みなので、この節の生成コマンドは再実行しません。まだ存在しない場合だけ実行します。

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
cd ..

create-viteをインストールするか確認された場合はyを入力します。

  • frontend: 作成するフォルダー名
  • react-ts: ReactとTypeScriptのテンプレート
  • npm install: 依存パッケージを入れ、package-lock.jsonを作る
  • cd ..: 一つ上へ戻る

4-4. Honoを作る

8週間ルートでは第3週にbackendを作成済みなので、Honoの生成コマンドは再実行しません。まだ存在しない場合だけ実行します。

npm create hono@latest backend -- --template nodejs --pm npm --install

Node.js用Honoテンプレートを作り、依存パッケージもインストールします。確認された場合はyを入力します。

PostgreSQL接続用のpgと型定義を追加します。

cd backend
npm install pg
npm install --save-dev @types/pg
cd ..
  • pg: Node.jsからPostgreSQLへ接続するライブラリ
  • @types/pg: pgのTypeScript型定義
  • --save-dev: 開発や型検査で使う依存関係として追加する

Dockerを使っていても、ここではPC側でnpmを実行します。

  • PC側のnpm: テンプレート、package-lock.json、エディターが参照する型情報を準備する
  • コンテナ側のnpm ci: ロックファイルどおりにLinux用の依存関係を再現する
  • PC側のnode_modules: .dockerignoreによりイメージへコピーしない

将来@latestのテンプレートがNode.js 24をサポートしなくなった場合は、使用するLTS版とDockerfileのFROM node:...を同時に更新してください。

4-5. DB用フォルダーを作る

mkdir -p database

-pを付けると、すでにdatabaseが存在していてもエラーにせず、そのまま使えます。

完成時の主な構成です。ここにあるDockerfile、compose.yamlinit.sqlは、後の章でこれから作るファイルです。

greeting-db-app/
├── compose.yaml
├── database/
│   └── init.sql
├── frontend/
│   ├── .dockerignore
│   ├── Dockerfile
│   └── src/
│       └── App.tsx
└── backend/
    ├── .dockerignore
    ├── Dockerfile
    └── src/
        └── index.ts

frontendbackendには、表示していないテンプレート生成ファイルもあります。削除せず、そのまま使います。

エディターでgreeting-db-app全体を開いてください。


5. PostgreSQLのテーブルを作る

database/init.sql(新規作成)

CREATE TABLE IF NOT EXISTS greetings (
  id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name VARCHAR(100) NOT NULL CHECK (btrim(name) <> '')
);

SQLはTypeScriptではなく、DBへ命令する別の言語です。

5-1. CREATE TABLE

  • CREATE TABLE: テーブルを作る
  • IF NOT EXISTS: 同名のテーブルがない場合だけ作る
  • greetings: テーブル名
  • ( ... ): 列と制約を書く
  • ;: SQL文の終わり

5-2. id

id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
  • INTEGER: 整数型
  • GENERATED ALWAYS AS IDENTITY: DBが番号を自動発行する
  • PRIMARY KEY: 各行を一意に識別する主キー

アプリはIDを自分で決めません。PostgreSQLが行を一意に識別する番号を自動発行します。通常は番号が増えますが、失敗した処理などにより欠番が生じることがあるため、「欠番のない連番」は保証されません。

5-3. name列と制約

name VARCHAR(100) NOT NULL CHECK (btrim(name) <> '')
  • VARCHAR(100): 100文字までの文字列型
  • NOT NULL: NULLを保存できない
  • CHECK: 保存前に条件を確認する
  • btrim(name): 前後の半角スペースを除いた値
  • <>: SQLで「等しくない」

つまり、100文字を超える名前、NULL、半角スペースだけの名前を保存できません。Honoでも検証しますが、DBにも最後の制約を置きます。

5-4. 初期化SQLの実行タイミング

PostgreSQL公式イメージは、データ領域が空の初回起動時に/docker-entrypoint-initdb.d内のSQLを実行します。

init.sqlを後から変更しても既存Volumeには自動で再実行されません。実務ではマイグレーションを使います。この教材では最初のテーブル作成だけを扱います。


6. Dockerfileを作る

Dockerfileは拡張子を付けず、名前をそのままDockerfileにします。

6-1. バックエンド

backend/Dockerfile(新規作成)

FROM node:24-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

6-2. フロントエンド

frontend/Dockerfile(新規作成)

FROM node:24-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]

6-3. Dockerfileの命令

命令 この教材での意味
FROM node:24-alpine Node.js 24を含む小さめのLinuxイメージを土台にする
WORKDIR /app 以降の作業場所を/appにする
COPY package*.json ./ 依存関係の定義とロックファイルを先にコピーする
RUN npm ci ロックファイルどおりにパッケージを入れる
COPY . . ソースコードをイメージへコピーする
EXPOSE コンテナが使う予定のポートを記録する
CMD コンテナ起動時の標準コマンドを決める

依存ファイルをソースより先にコピーすると、依存関係が変わっていない再ビルドでDockerのキャッシュを利用しやすくなります。

npm installは開発中に依存関係を追加するとき、npm ciは既存ロックファイルから再現可能にインストールするときに使います。

EXPOSEだけではPCからアクセス可能になりません。実際のポート転送はcompose.yamlportsで設定します。

Viteは標準ではコンテナ内部のlocalhostだけで待ち受けます。PCのブラウザからアクセスするため、--host 0.0.0.0を付けます。

6-4. .dockerignore

backend/.dockerignorefrontend/.dockerignore(同じ内容)

node_modules
dist
.git
*.log
.env
.env.*
!.env.example

Dockerは、イメージを作る際に**ビルドコンテキスト(build context)**というファイル範囲を受け取ります。.dockerignoreは、その範囲からイメージ作成に不要なファイルを除外する設定です。

node_modulesはコンテナ内でnpm ciし直します。.env.env.*は秘密を含む可能性があるためイメージへコピーせず、値を入れない見本の.env.exampleだけは含められるようにします。


7. Docker Composeを設定する

compose.yaml(プロジェクトルートへ新規作成)

services:
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_DB: greeting_app
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: local_password
    volumes:
      - postgres_data:/var/lib/postgresql
      - ./database/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d greeting_app"]
      interval: 5s
      timeout: 5s
      retries: 10

  backend:
    build: ./backend
    environment:
      DATABASE_URL: postgresql://app_user:local_password@db:5432/greeting_app
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      db:
        condition: service_healthy

  frontend:
    build: ./frontend
    ports:
      - "127.0.0.1:5173:5173"
    depends_on:
      - backend

volumes:
  postgres_data:

YAMLはインデントに意味があります。タブではなくスペースを使い、同じ階層は同じ深さにそろえます。

  • key: value: 名前と値の組み合わせ
  • key:の下へインデントした行: そのキーに含まれる設定
  • - value: リストの一項目

現在のCompose Specificationでは、古い例にある先頭のversion:は不要です。

7-1. dbサービス

image: postgres:18-alpine

PostgreSQL公式イメージを使います。18でメジャーバージョンを固定し、latestは使いません。

environment:
  POSTGRES_DB: greeting_app
  POSTGRES_USER: app_user
  POSTGRES_PASSWORD: local_password

初回起動時に作るデータベース名、ユーザー名、パスワードです。

PostgreSQL公式イメージは、POSTGRES_USERで指定したユーザーを初期管理用の強い権限を持つユーザーとして作ります。

app_userlocal_passwordはローカル学習専用です。本番ではファイルへパスワードを直接書かず、公開先が秘密値を安全に渡すためのシークレット管理機能と、必要最小限の権限を持つ専用ユーザーを使います。

これらのPOSTGRES_*設定も、空のVolumeを初期化するときだけ使われます。既存Volumeがある状態で値を変えても、作成済みのDB名・ユーザー名・パスワードは自動変更されません。

volumes:
  - postgres_data:/var/lib/postgresql
  - ./database/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro

一行目はDocker管理の名前付きVolumeです。PostgreSQL 18以降では/var/lib/postgresqlへマウントします。17以前の古い例で見る/var/lib/postgresql/dataとは異なります。

二行目はPC側のinit.sqlをコンテナへ見せる bind mount です。:roはread-onlyを表します。

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U app_user -d greeting_app"]
  interval: 5s
  timeout: 5s
  retries: 10

コンテナのプロセスが起動しても、PostgreSQLがすぐ接続を受け付けられるとは限りません。pg_isreadyで準備完了を確認します。

DBの5432portsでPCへ公開していません。HonoはComposeネットワークから接続できます。

7-2. backendサービス

build: ./backend

backend/Dockerfileからイメージを作ります。./backendはビルドコンテキストであり、Dockerのビルドへ渡すファイル範囲でもあります。

DockerfileのCOPY . .は、このビルドコンテキスト内をコピーします。backend/.dockerignoreも同じ範囲へ適用されます。

environment:
  DATABASE_URL: postgresql://app_user:local_password@db:5432/greeting_app

接続文字列は次の形です。

postgresql://ユーザー:パスワード@ホスト:ポート/データベース

ホスト名はlocalhostではなくComposeサービス名dbです。

ports:
  - "127.0.0.1:3000:3000"

HOST_IP:HOST_PORT:CONTAINER_PORTの順です。127.0.0.1を付け、このPCからだけアクセスできるようにします。

depends_on:
  db:
    condition: service_healthy

dbのhealthcheckが成功してからbackendを起動します。単純なdepends_onだけでは、DBが接続可能になるまで待つ保証はありません。

7-3. frontendサービス

frontend:
  build: ./frontend
  ports:
    - "127.0.0.1:5173:5173"
  depends_on:
    - backend

frontend/Dockerfileからイメージを作り、PCの5173番をコンテナの5173番へ転送します。

フロントエンドは起動直後にAPIを呼ばないため、backendはコンテナの起動順だけを指定しています。

7-4. Composeが作るもの

イメージ
├── frontend用
├── backend用
└── postgres:18-alpine

コンテナ
├── frontend
├── backend
└── db

デフォルトネットワーク
└── 3コンテナを接続

名前付きVolume
└── postgres_data

7-5. チェックポイント1:DBだけを起動する

ここまでで、まずPostgreSQLだけを確認します。プロジェクトルートで実行してください。

docker compose config
docker compose up db

database system is ready to accept connectionsと表示されたら、別のターミナルを開いてテーブルを確認します。

docker compose exec db psql -U app_user -d greeting_app -c '\dt'

一覧にgreetingsがあれば、Compose、Volume、初期化SQLまで成功しています。

確認後はdocker compose up dbのターミナルでCtrl + Cを押してください。コンテナが停止しても、作成済みのVolumeは残ります。


8. HonoからPostgreSQLへ保存する

この章では、最初に完成コードをコピーしてAPIを起動し、その後の8-1〜8-13でコードを上から順に読み解きます。初見の構文をすべて理解してからコピーする必要はありません。動作確認後に、各節をコードと照合してください。

編集する中心ファイルは一つです。

backend/src/index.ts(全置換)

import { serve } from '@hono/node-server'
import { Pool } from 'pg'
import { Hono } from 'hono'
import { cors } from 'hono/cors'

type SavedGreeting = {
  id: number
  name: string
}

const databaseUrl = process.env.DATABASE_URL

if (databaseUrl === undefined) {
  throw new Error('DATABASE_URL が設定されていません')
}

const pool = new Pool({ connectionString: databaseUrl })

pool.on('error', (error) => {
  console.error('PostgreSQL connection error:', error)
})

const app = new Hono()

app.use(
  '/api/*',
  cors({
    origin: 'http://localhost:5173',
    allowMethods: ['POST', 'OPTIONS'],
    allowHeaders: ['Content-Type'],
  }),
)

app.post('/api/greetings', async (c) => {
  const body: unknown = await c.req.json().catch(() => null)

  if (
    typeof body !== 'object' ||
    body === null ||
    !('name' in body) ||
    typeof body.name !== 'string' ||
    body.name.trim() === ''
  ) {
    return c.json({ error: '名前を入力してください' }, 400)
  }

  const name = body.name.trim()

  if (name.length > 100) {
    return c.json({ error: '名前は100文字以内で入力してください' }, 400)
  }

  const result = await pool.query<SavedGreeting>(
    `INSERT INTO greetings (name)
     VALUES ($1)
     RETURNING id, name`,
    [name],
  )
  const savedGreeting = result.rows[0]

  if (savedGreeting === undefined) {
    throw new Error('保存結果を取得できませんでした')
  }

  return c.json(
    {
      id: savedGreeting.id,
      message: `こんにちは、${savedGreeting.name}さん!`,
    },
    201,
  )
})

app.onError((error, c) => {
  console.error(error)
  return c.json({ error: 'サーバーでエラーが発生しました' }, 500)
})

serve(
  {
    fetch: app.fetch,
    hostname: '0.0.0.0',
    port: 3000,
  },
  (info) => {
    console.log(`API: http://localhost:${info.port}`)
  },
)

8-1. 読み込む機能

import { serve } from '@hono/node-server'
import { Pool } from 'pg'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
  • Hono: APIのルートを作る
  • serve: HonoをNode.jsのサーバーとして起動する
  • cors: Reactのオリジンからレスポンスを読めるようにする
  • Pool: PostgreSQL接続を再利用するコネクションプール

DBへ問い合わせるたびに接続を一から作るのではなく、Poolが接続を管理して再利用します。このアプリではPoolを一つだけ作ります。

8-2. DBの一行を表す型

type SavedGreeting = {
  id: number
  name: string
}

INSERT ... RETURNINGでPostgreSQLから返る一行の形です。この型はTypeScript上の約束であり、DBのテーブル定義を自動取得しているわけではありません。

8-3. 環境変数を確認する

const databaseUrl = process.env.DATABASE_URL

if (databaseUrl === undefined) {
  throw new Error('DATABASE_URL が設定されていません')
}

process.envはNode.jsから環境変数を読むオブジェクトです。ComposeがDATABASE_URLを渡します。

環境変数がない可能性をifで確認します。ない場合に誤った接続先で動き続けず、起動時に明確なエラーを出します。

8-4. Poolを作る

const pool = new Pool({ connectionString: databaseUrl })

new Pool(...)でコネクションプールを作ります。Poolは必要になったときに接続を作るため、この行だけではまだSQLを実行しません。

pool.on('error', (error) => {
  console.error('PostgreSQL connection error:', error)
})

待機中の接続で予期しないエラーが起きた場合、ログへ表示します。(error) => { ... }はイベント発生時に呼ばれるコールバック関数です。

8-5. CORS

オリジンは、URLの通信方法、ホスト名、ポートを組み合わせた通信元の単位です。http://localhost:5173http://localhost:3000は、ポートが違うため別オリジンです。

app.use(
  '/api/*',
  cors({
    origin: 'http://localhost:5173',
    allowMethods: ['POST', 'OPTIONS'],
    allowHeaders: ['Content-Type'],
  }),
)

Reactはlocalhost:5173、Honoはlocalhost:3000なので、ブラウザから見ると異なるオリジンです。Hono側で、Reactの画面がAPIレスポンスを読めるようにします。

OPTIONSは、ブラウザがJSONのPOSTを送る前に利用可否を確認するプリフライトリクエストで使われます。

CORSは認証やアクセス制御ではありません。curlや別のサーバーからの通信を防ぐ仕組みではありません。

8-6. POSTとJSON

app.post('/api/greetings', async (c) => {
  const body: unknown = await c.req.json().catch(() => null)
})
  • post: HTTPのPOSTを受け取る
  • /api/greetings: エンドポイントのパス
  • c: HonoのContext
  • c.req.json(): リクエストボディをJSONとして読む
  • catch(() => null): 壊れたJSONならnullにする
  • unknown: まだ安全な型だと確認していない値

8-7. 入力を検証する

if (
  typeof body !== 'object' ||
  body === null ||
  !('name' in body) ||
  typeof body.name !== 'string' ||
  body.name.trim() === ''
) {
  return c.json({ error: '名前を入力してください' }, 400)
}

順番に次を確認します。

  1. JSONがオブジェクトか
  2. nullではないか
  3. nameプロパティがあるか
  4. nameが文字列か
  5. 前後の空白を除いて空でないか

型や空文字の検証を通過した後、長さも確認します。

const name = body.name.trim()

if (name.length > 100) {
  return c.json({ error: '名前は100文字以内で入力してください' }, 400)
}

不正な場合はエラーJSONと400 Bad Requestを返します。Reactの入力制限を回避して直接APIを呼ばれても、Hono側で拒否できます。

8-8. パラメーター化クエリで保存する

const name = body.name.trim()
const result = await pool.query<SavedGreeting>(
  `INSERT INTO greetings (name)
   VALUES ($1)
   RETURNING id, name`,
  [name],
)

SQLの意味です。

  • INSERT INTO greetings (name): greetingsテーブルのname列へ追加する
  • VALUES ($1): 一つ目のパラメーターを値として使う
  • [name]: $1に対応する値
  • RETURNING id, name: 保存後のIDと名前を返す
  • await: DBの処理完了を待つ

利用者の入力をSQL文字列へ直接連結すると、SQLインジェクションの原因になります。

// 危険な考え方: 利用者の入力をSQL文字列へ直接入れない
const unsafeSql =
  `INSERT INTO greetings (name) VALUES ('${name}')`

完成コードではSQL本文の$1と値の[name]を分けています。pgとPostgreSQLが値を安全に渡すため、名前がSQLの命令として解釈されません。

パラメーター化できるのは値です。利用者の入力をテーブル名や列名として組み立てる処理は別の対策が必要です。この教材では行いません。

8-9. 保存結果を確認する

const savedGreeting = result.rows[0]

if (savedGreeting === undefined) {
  throw new Error('保存結果を取得できませんでした')
}

result.rowsは結果行の配列です。今回は一行だけ挿入するため、先頭のrows[0]を読みます。

万一一行も返らなかった場合はエラーにします。ifの後ではsavedGreetingが存在すると型が絞り込まれます。

8-10. 201 Createdを返す

return c.json(
  {
    id: savedGreeting.id,
    message: `こんにちは、${savedGreeting.name}さん!`,
  },
  201,
)

新しい行の作成に成功したため201 Createdを返します。通常の取得成功でよく使う200 OKと区別します。

{
  "id": 1,
  "message": "こんにちは、Taroさん!"
}

8-11. 予期しないエラー

app.onError((error, c) => {
  console.error(error)
  return c.json({ error: 'サーバーでエラーが発生しました' }, 500)
})

DB接続失敗など、ルート処理中の予期しないエラーをログへ出し、利用者には500 Internal Server Errorを返します。パスワードや内部エラーの詳細はレスポンスへ含めません。

8-12. コンテナ外から接続できるように起動する

serve({
  fetch: app.fetch,
  hostname: '0.0.0.0',
  port: 3000,
})

Honoをコンテナの3000番で起動します。hostname: '0.0.0.0'により、Dockerのポート転送とComposeネットワークから接続できます。

8-13. チェックポイント2:DBとAPIを起動する

次はdbbackendだけを起動し、画面を使わずAPIを確認します。プロジェクトルートで実行してください。

docker compose up --build db backend

dbがhealthyになり、backendAPI: http://localhost:3000と表示されたら、別のターミナルから送信します。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"API checkpoint"}'

201 Createdと保存番号が返れば、Reactより手前のHono → PostgreSQLまで成功しています。

確認後はComposeログのターミナルでCtrl + Cを押してください。次章でフロントエンドを追加します。


9. Reactで保存結果を表示する

この章も、最初に完成コードをコピーし、その後の9-1〜9-12で役割ごとに読み解きます。

Reactでは、画面を役割ごとのコンポーネントに分けて組み立てます。今回は次の2つを使います。

App(親コンポーネント)
├── form(名前を入力して送信するHTML要素)
└── ResultMessage(結果があるときに表示する子コンポーネント)
    └── p(文章を表示するHTML要素)

Appが入力、API通信、Stateを担当し、ResultMessageは受け取った文章の表示だけを担当します。Propsによる親子間のデータ受け渡しを学ぶため、短い表示処理を意図的に子コンポーネントへ分けています。ファイル数を増やさないよう、2つのコンポーネントは同じファイルに記述します。

編集する中心ファイルはこちらも一つです。

frontend/src/App.tsx(全置換)

import { useState } from 'react'
import type { FormEvent } from 'react'

type GreetingResponse = {
  id: number
  message: string
}

type ResultMessageProps = {
  message: string
}

function ResultMessage(props: ResultMessageProps) {
  return <p>{props.message}</p>
}

function App() {
  const [name, setName] = useState('')
  const [message, setMessage] = useState('')

  async function handleSubmit(
    event: FormEvent<HTMLFormElement>,
  ): Promise<void> {
    event.preventDefault()

    try {
      const response = await fetch(
        'http://localhost:3000/api/greetings',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ name }),
        },
      )

      if (!response.ok) {
        throw new Error(`API エラー: ${response.status}`)
      }

      const data = (await response.json()) as GreetingResponse
      setMessage(`保存番号 ${data.id}: ${data.message}`)
    } catch {
      setMessage('保存に失敗しました')
    }
  }

  return (
    <main>
      <h1>あいさつ保存アプリ</h1>

      <form onSubmit={handleSubmit}>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          value={name}
          maxLength={100}
          required
          onChange={(event) => setName(event.target.value)}
        />
        <button type="submit">保存</button>
      </form>

      {message !== '' && <ResultMessage message={message} />}
    </main>
  )
}

export default App

見た目を整えるCSSは追加しません。App.tsxから元のApp.cssのimportもなくなるため、App.cssの内容は使われません。Viteのsrc/index.cssはそのままで構いません。

9-1. コンポーネントとは

コンポーネントは、UIを役割ごとに分割した構成単位です。Reactでは複数のコンポーネントを組み合わせて、1つの画面を作ります。

この教材のAppResultMessageは、どちらもJSXを返す関数です。このような関数を関数コンポーネントと呼びます。

function ResultMessage(props: ResultMessageProps) {
  return <p>{props.message}</p>
}
  • コンポーネント名はResultMessageのように大文字から始める
  • JSXを返すと、その内容が画面に表示される
  • 一つのファイルに複数のコンポーネントを書いてもよい

formpのような小文字のタグはHTML要素です。ResultMessageのような大文字のタグは、自分で作ったReactコンポーネントです。

9-2. 親コンポーネントと子コンポーネント

別のコンポーネントをJSX内で使う側を親コンポーネント、使われる側を子コンポーネントと呼びます。

function App() {
  // 途中省略

  return (
    <main>
      {message !== '' && <ResultMessage message={message} />}
    </main>
  )
}

ここではAppが親、ResultMessageが子です。親子関係はクラスの継承ではなく、「どのコンポーネントのJSX内で使われているか」で決まります。AppResultMessageを関数として直接呼ばず、<ResultMessage />というJSXでUIの構成単位として組み込みます。

9-3. Propsで親から子へ値を渡す

Propsは、親コンポーネントから子コンポーネントへ渡す値です。HTML要素の属性に似た形で記述します。

<ResultMessage message={message} />

左側のmessageはPropsの名前、{message}Appが持つStateの値です。子には次のオブジェクトとして渡されます。

type ResultMessageProps = {
  message: string
}

子コンポーネントでは、関数の引数propsとして受け取ります。

function ResultMessage(props: ResultMessageProps) {
  return <p>{props.message}</p>
}

ResultMessagePropsにより、次の間違いはTypeScriptの型エラーになります。

<ResultMessage message={123} />

messagestringと決めたのに、numberを渡しているためです。

Propsの型はTypeScriptによる開発時の検査です。実行時にネットワークの値を検証する仕組みではありません。

Propsは子から書き換える値ではありません。親から子へ値が流れることを、Reactの一方向データフローと呼びます。結果表示を変更したい場合は、子がPropsを書き換えるのではなく、親のAppsetMessage(...)でStateを更新します。

PropsとStateの違いを整理します。

Props State
値の持ち主 親コンポーネント そのStateを宣言したコンポーネント
値の受け取り方 親から渡される useStateで作る
値を変える方法 子から直接変更しない State更新関数を使う
今回の例 ResultMessageが受け取るmessage Appが持つnamemessage

9-4. レスポンス型

type GreetingResponse = {
  id: number
  message: string
}
  • id: PostgreSQLが発行した保存番号
  • message: Honoが作った挨拶

この型はHonoやPostgreSQLから自動生成されたものではありません。

9-5. Stateと再レンダー

const [name, setName] = useState('')
const [message, setMessage] = useState('')

Stateはコンポーネント内部で保持する値です。Stateを更新すると、Reactはコンポーネントを再レンダーします。初期値が空文字なので、TypeScriptは両方をstringと推論します。

State 内容
name 入力中の名前
message 保存結果または失敗メッセージ

今回はhandleSubmitが結果を更新し、ResultMessageがその結果を表示します。その両方をまとめる親のAppmessage Stateを持ちます。

setMessage(...)を呼ぶとStateが更新され、ReactはAppをもう一度実行して新しいJSXを作ります。これを再レンダーと呼びます。

setMessage(...) を呼ぶ
  ↓
App が新しい message で再レンダーされる
  ↓
新しい message がPropsとして ResultMessage へ渡る
  ↓
Reactが必要な画面部分を更新する

再レンダーはページ全体の再読み込みではありません。Reactは前後の結果を比べ、ブラウザが画面構造を表すDOMのうち、必要な部分だけを更新します。コンポーネントの関数は再レンダーのたびに実行されるため、初回だけ実行される関数ではありません。

9-6. フォームイベント

async function handleSubmit(
  event: FormEvent<HTMLFormElement>,
): Promise<void> {
  event.preventDefault()
}
  • FormEvent<HTMLFormElement>: form送信イベントの型
  • Promise<void>: 非同期処理が完了するが、値は返さない
  • preventDefault(): ブラウザ標準のページ再読み込みを止める

9-7. JSONを送る

const response = await fetch(
  'http://localhost:3000/api/greetings',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name }),
  },
)
  • fetch: HTTPリクエストを送るブラウザ標準関数
  • method: 'POST': POSTメソッドを使う
  • Content-Type: ボディがJSONだと伝える
  • JSON.stringify: オブジェクトを送信用JSON文字列へ変換する
  • { name }: { name: name }の短縮形

Reactのコードはブラウザで実行されるため、接続先はbackend:3000ではなくlocalhost:3000です。

9-8. HTTPエラーを確認する

if (!response.ok) {
  throw new Error(`API エラー: ${response.status}`)
}

fetchは400や500が返っても、それだけではcatchへ移りません。response.okを確認し、自分でエラーを発生させます。

201 Createdは成功範囲なのでresponse.oktrueです。

9-9. JSONを読み、Stateを更新する

const data = (await response.json()) as GreetingResponse
setMessage(`保存番号 ${data.id}: ${data.message}`)

response.json()でJSONをJavaScriptの値へ変換します。as GreetingResponseは実行時の検証ではなく型アサーションです。

9-10. 失敗を処理する

try {
  // API 通信
} catch {
  setMessage('保存に失敗しました')
}

Honoに接続できない、400や500が返る、JSONを読めない、という場合にcatchへ移ります。詳しい原因はNetworkタブやComposeのログで確認します。

9-11. JSX

<form onSubmit={handleSubmit}>
  <input
    value={name}
    maxLength={100}
    required
    onChange={(event) => setName(event.target.value)}
  />
</form>
  • onSubmit: フォーム送信時に関数を呼ぶ
  • value={name}: Stateの値を入力欄へ表示する
  • onChange: 入力が変わるたびStateを更新する
  • maxLength={100}: ブラウザの入力を100文字までにする
  • required: 空のまま送信できないようにする
  • {}: JSX内でTypeScriptの式を使う

onChangeeventは、inputのイベントハンドラーで使われているという周囲の情報から型が推論されます。そのため、この例ではeventの型注釈を省略できます。

{message !== '' && <ResultMessage message={message} />}

messageが空文字でない場合だけ、子コンポーネントのResultMessageを画面へ含めます。これを条件付きレンダリングと呼びます。

export default Appは、main.tsxAppを読み込めるように公開します。

9-12. どこまでコンポーネントに分けるか

すべてのHTML要素を別コンポーネントにする必要はありません。次のような部分は分ける候補になります。

  • 一つの名前で説明できる役割がある
  • 同じ表示を複数の場所で使う
  • コードが長くなり、別に読む方が分かりやすい
  • その部分だけに固有のPropsや処理がある

今回のResultMessageは短く、一度しか使いません。実務ならAppに直接書いても問題ありませんが、この教材では親子関係とPropsを実コードで確認するために一つだけ分けています。一方、ラベルやボタンまで一つずつ分けると流れが追いにくくなるため、Appの中に残しています。

コンポーネントとファイルは同じ意味ではありません。今回は一つのファイルに二つのコンポーネントを書きました。再利用する場所が増えたりコードが大きくなったりしたら、ResultMessage.tsxのような別ファイルへ移し、exportimportで接続できます。


10. チェックポイント3:3つのサービスでアプリを動かす

10-1. 設定を検証する

greeting-db-app直下で実行します。

docker compose config

YAMLのインデントや設定名に問題がある場合、この時点でエラーが表示されます。

展開後の設定には学習用パスワードも表示されるため、出力を公開場所へ貼り付けないでください。

10-2. ビルドして起動する

docker compose up --build
  • up: Composeのサービスを作成・起動する
  • --build: frontendbackendのイメージをビルドする

初回はNode.jsとPostgreSQLのイメージをダウンロードするため、時間がかかります。ログが流れ続ける状態は正常です。

DBのhealthcheckが成功した後にbackend、その後frontendが起動します。

準備完了時の目印になるログです。細かな文言や接頭辞はバージョンによって変わります。

db-1       | database system is ready to accept connections
backend-1  | API: http://localhost:3000
frontend-1 | Local: http://localhost:5173/

コードを変更した場合は、もう一度docker compose up --buildしてイメージへ反映します。この教材ではソースコードをコンテナへbind mountしていません。

10-3. コンテナの状態を見る

別のターミナルを開き、同じgreeting-db-app直下から実行します。

docker compose ps

三つのサービスが起動し、dbhealthyなら準備完了です。dbのPORTSに5432/tcpとだけ表示されるのは正常で、PC側へ公開していません。

10-4. ブラウザで確認する

http://localhost:5173 を開きます。

  1. Taroと入力する
  2. 「保存」を押す
  3. 保存番号N: こんにちは、Taroさん!と表示される(NはDBが発行した番号)
  4. 別の名前を保存すると番号が増える

10-5. APIを単独で確認する

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"Taro"}'

成功例です。すでに保存している件数によってidは変わります。

HTTP/1.1 201 Created
Content-Type: application/json

{"id":1,"message":"こんにちは、Taroさん!"}

空白だけの名前も試します。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"   "}'

400 Bad RequestとエラーJSONが返れば、入力検証が動いています。

上限を一文字超える、101文字の名前も試します。値は1234567890を10回並べ、最後にXを一つ付けたものです。

curl -i -X POST http://localhost:3000/api/greetings \
  -H 'Content-Type: application/json' \
  -d '{"name":"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890X"}'

これも400 Bad Requestになれば、Honoの100文字制限を確認できました。

10-6. ブラウザで通信を見る

ブラウザの開発者ツールでNetworkタブを開き、保存します。

項目 期待する値
Request URL http://localhost:3000/api/greetings
Request Method POST
Status Code 201
Request Payload {"name":"入力した名前"}
Response {"id":番号,"message":"こんにちは、入力した名前さん!"}

11. PostgreSQLのデータと永続化を確認する

11-1. psqlでテーブルを見る

PostgreSQLのコマンドラインツールpsqldbコンテナ内にあります。PCへ別途インストールする必要はありません。

docker compose exec db psql -U app_user -d greeting_app \
  -c 'SELECT id, name FROM greetings ORDER BY id;'
  • docker compose exec db: 起動中のdbコンテナでコマンドを実行する
  • psql: PostgreSQLのクライアント
  • -U app_user: DBユーザー
  • -d greeting_app: 接続するデータベース
  • -c: 続くSQLを実行する
  • SELECT: テーブルから行を読み取る
  • ORDER BY id: IDの小さい順に並べる

チェックポイント2を実行した場合はAPI checkpointも含め、これまで保存した名前が行として表示されます。IDや行数は、それまでに保存した内容によって変わります。

Reactが直接SQLを実行したのではありません。React → Hono → PostgreSQLという経路で保存された行です。

11-2. 停止と再起動

docker compose upのターミナルでCtrl + Cを押し、停止します。その後、プロジェクトルートで実行します。

docker compose down

downはコンテナとComposeネットワークを削除しますが、名前付きVolumeは残します。

再び起動します。

docker compose up

別のターミナルから先ほどのSELECTを実行してください。保存した行が残っていれば、Volumeによる永続化を確認できました。新しい名前を保存すると、IDは続きの番号になります。

11-3. Volumeと初期化SQL

init.sqlpostgres_dataが空の初回だけ実行されます。

初回 up
  ├─ PostgreSQLのデータ領域を作る
  ├─ init.sql を実行して greetings テーブルを作る
  └─ Volumeへ保存する

2回目以降の up
  └─ 既存Volumeを読み、init.sqlは再実行しない

警告: docker compose down -vはVolumeも削除し、保存した名前をすべて消します。init.sqlから完全にやり直す必要がある場合だけ実行してください。通常の停止にはdocker compose downを使います。

実務では、保存データを消さずにテーブル構造を更新するためマイグレーションを使います。この最小教材では扱いません。


12. コード全体の流れ

保存ボタンを押した後は、次の順で処理されます。

  1. ReactのhandleSubmitが呼ばれる
  2. fetchが名前をJSONにしてlocalhost:3000へ送る
  3. DockerがPCの3000番からbackendコンテナへ転送する
  4. Honoのapp.post('/api/greetings', ...)が受け取る
  5. HonoがJSONをunknownとして読み、名前を検証する
  6. pg$1[name]を分けたクエリを送る
  7. Composeネットワークでbackendからdb:5432へ届く
  8. PostgreSQLが行をVolumeへ保存し、IDと名前を返す
  9. Honoが201とJSONを返す
  10. ReactのAppGreetingResponseとして読み、Stateを更新する
  11. Appが再レンダーされ、新しいmessageをPropsとして子へ渡す
  12. ResultMessageが保存番号付きの挨拶を画面へ表示する

通信の種類を分けて整理します。

区間 主な形式
React → Hono HTTPとJSON
Hono → PostgreSQL PostgreSQLの通信とSQL
PostgreSQL → Volume DBのデータファイル

TypeScriptの型だけで、この全区間が自動的に保証されるわけではありません。そのため、Honoの実行時検証、パラメーター化クエリ、DB制約も組み合わせます。


13. よくあるエラー

13-1. まず確認するコマンド

docker compose ps
docker compose logs db
docker compose logs backend
docker compose logs frontend

psで状態、logsで各サービスの出力を確認します。

13-2. 症状と対処

症状 主な原因と対処
Docker daemonに接続できない Docker Desktopを起動する
compose.yamlのエラー docker compose configでインデントやキー名を確認する
frontend / backendのビルド失敗 package-lock.jsonがあるか、npm install済みか確認する
dbがunhealthy docker compose logs dbでユーザー名、DB名、初期化SQLを確認する
DATABASE_URLが設定されていません backendenvironmentを確認する
getaddrinfo ENOTFOUND db backendをPC上で直接起動していないか確認する。この構成はCompose内でdbを使う
relation "greetings" does not exist init.sqlのパスと初回のDBログを確認する。初回SQLが失敗しており学習データを消してよい場合だけ、11-3の警告を読んでVolumeから初期化し直す
Failed to fetch backendが起動中か、3000番へ公開されているか確認する
CORSエラー http://localhost:5173で開いているか、Honoのoriginを確認する
400 nameが文字列で、空白だけではなく100文字以内か確認する
500 backendのログとdbの状態を確認する
コード変更が反映されない docker compose up --buildでイメージを作り直す
init.sqlの変更が反映されない 初期化SQLは空のVolumeでのみ実行される。第11章を確認する
3000番または5173番が使用中 既存の開発サーバーやComposeプロジェクトを停止する

13-3. 調査する順番

  1. docker compose config
  2. docker compose ps
  3. docker compose logs db
  4. docker compose logs backend
  5. curlでAPI単体
  6. ブラウザのNetworkタブ
  7. ブラウザのConsole

土台になるDB、次にAPI、最後に画面という依存関係の順で確認すると、原因を絞りやすくなります。

メンターに相談する目安

この順番で一度確認しても原因を特定できない場合は、「達成したいこと」「再現手順」「期待した結果」「実際の結果」「最初のエラー」「試したこと」をメンターへ共有してください。接続URL、パスワード、トークンは伏せます。答えだけでなく、「次にどのログや値を確認すべきか」を質問すると、調査の進め方も学べます。


14. TypeScriptとビルドを確認する

コンテナが起動中の状態で、別ターミナルから実行します。

docker compose exec frontend npm run build
docker compose exec backend npm run build

どちらもエラーなく終了すれば、完成コードの型検査とビルドが通っています。

npm run devが動いていても、型エラーがないとは限りません。最後に必ずビルドを確認してください。

動作チェックリスト

  • docker compose configが成功する
  • frontendbackenddbが起動する
  • dbhealthyになる
  • ブラウザから名前を保存できる
  • APIが201 Createdを返す
  • 空白名で400 Bad Requestが返る
  • psqlで保存した行を確認できる
  • docker compose downと再起動後も行が残る
  • frontendbackendnpm run buildが成功する

15. 小さな演習

この教材はソースコードをコンテナへbind mountしていません。ソースコードを変更する演習では、変更前のdocker compose upが動いていればCtrl + Cで停止し、変更後に次のコマンドでイメージを作り直してください。

docker compose up --build

演習1:保存後に入力欄を空にする

保存に成功したら、入力欄を空にしてください。

解答例

setMessage(...)の直後に追加します。

setName('')

演習2:保存件数をSQLで数える

greetingsテーブルの行数を調べるSQLを考えてください。

解答例
docker compose exec db psql -U app_user -d greeting_app \
  -c 'SELECT COUNT(*) AS greeting_count FROM greetings;'

COUNT(*)は行数を数え、AS greeting_countは結果列へ名前を付けます。

演習3:挨拶を変更する

Honoのレスポンスを次の形式へ変更してください。

Taroさん、DockerとDBを一緒に学びましょう!
解答例
message:
  `${savedGreeting.name}さん、DockerとDBを一緒に学びましょう!`,

変更後は再ビルドします。

docker compose up --build

発展課題

最小アプリを理解した後は、次の順で機能を増やせます。

  1. GET /api/greetingsを追加して履歴を読む
  2. Reactで履歴をmapして表示する
  3. 作成日時の列を追加する
  4. マイグレーションツールを導入する
  5. 更新と削除を追加する

一度に全部を追加せず、一機能ごとにAPI、SQL、型、画面を確認してください。


16. まとめ

アプリの中心コードは二つです。

frontend/src/App.tsx
backend/src/index.ts

DockerとDBを使うため、次の設定ファイルも追加しました。

compose.yaml
frontend/Dockerfile
backend/Dockerfile
frontend/.dockerignore
backend/.dockerignore
database/init.sql

役割を整理します。

React       = コンポーネントを組み合わせ、入力と表示を担当
Props       = 親コンポーネントから子へ渡す読み取り用の値
State       = コンポーネントが保持し、更新すると再レンダーされる値
Hono        = 入力検証、DB操作、HTTPレスポンス
PostgreSQL  = 行をテーブルへ保存
pg          = HonoとPostgreSQLを接続
HTTP / JSON = ReactとHonoの通信
SQL         = HonoからDBへの命令
Dockerfile  = アプリ用イメージの作り方
Compose     = 3サービス、ネットワーク、Volumeの管理
Volume      = コンテナとは別にDBデータを保持
TypeScript  = コード中のデータの約束

特に重要なのは次の境界です。

  • ReactからHonoへはlocalhostとHTTP/JSON
  • AppがStateを持ち、PropsでResultMessageへ表示内容を渡す
  • HonoからPostgreSQLへはdbとSQL
  • TypeScriptの型はネットワークやDBの実データを自動検証しない
  • 利用者入力はHonoで実行時検証する
  • SQLには値を直接連結せず、パラメーター化クエリを使う
  • コンテナとDBの保存領域はVolumeで分ける

本番アプリには、認証・認可、シークレット管理、マイグレーション、バックアップ、監視、本番向けイメージ、HTTPSなども必要です。この教材では最小構成を使い、画面、API、DB、コンテナのデータフローに学習範囲を限定しています。


参考資料

構成の参考にした記事

公式ドキュメント

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?