Daily Hack

【初心者向け】Webアプリを作ろう STEP4:データベースを理解する(PostgreSQL)

プログラミング
#Web開発#初心者#PostgreSQL#データベース#Python#FastAPI

いよいよ最終ステップです。

STEP2でフロントエンド、STEP3でバックエンドを作りました。STEP4では**データベース(PostgreSQL)**を追加して、データを永続的に保存・取得できるWebアプリを完成させます。

ゴール

  • PostgreSQLにデータを登録できる
  • PostgreSQLからデータを取得してブラウザに表示できる
  • フロントエンド → バックエンド → データベース の全体の流れを体験する

全体の構成イメージ

【フロントエンド】
  ブラウザ(index.html / CSS / JS)
      ↕ HTTPリクエスト / レスポンス(JSON)
【バックエンド】
  Python(FastAPI)
      ↕ SQL(データの読み書き)
【データベース】← STEP4で追加
  PostgreSQL

今回作るもの:「ひとことメモ」アプリ

  • テキストボックスにメモを入力して保存ボタンを押すと、データベースに登録される
  • 画面を開くと、過去に保存したメモが一覧で表示される

作業フォルダの構成

webapp/
├── index.html      ← 更新する
├── style.css       ← 更新する
├── script.js       ← 更新する
└── main.py         ← 更新する

1. データベースの準備

PostgreSQLに接続する

ターミナルで以下を実行して、PostgreSQLに接続します:

psql -U postgres

パスワードを聞かれたら、STEP1でインストール時に設定したパスワードを入力してください。

接続に成功すると以下のプロンプトが表示されます:

psql (16.0)
Type "help" for help.

postgres=#

データベースを作成する

CREATE DATABASE webapp_db;
\c webapp_db

\c はデータベースに切り替えるコマンドです。

テーブルを作成する

今回は「メモ」を保存するテーブルを作ります:

CREATE TABLE memos (
  id        SERIAL PRIMARY KEY,
  content   TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
カラム名 説明
id SERIAL 自動で増える番号(1, 2, 3...)
content TEXT メモの本文
created_at TIMESTAMP 登録日時(自動で入る)

テーブルが作成されたか確認します:

\dt

memos テーブルが表示されればOKです。

psql を終了するには:

\q

2. PythonとPostgreSQLを接続する

PythonからPostgreSQLを操作するためのライブラリをインストールします:

pip install psycopg2-binary

3. バックエンド(main.py)を更新する

main.py を以下の内容に書き換えます:

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import psycopg2
from psycopg2.extras import RealDictCursor

app = FastAPI()

# CORSの設定(フロントエンドからのアクセスを許可)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# データベース接続の設定
DB_CONFIG = {
    "host": "localhost",
    "database": "webapp_db",
    "user": "postgres",
    "password": "自分のパスワードに変更してください",
    "port": 5432,
}

def get_connection():
    """データベース接続を返す"""
    return psycopg2.connect(**DB_CONFIG)


# リクエストボディの型定義
class MemoCreate(BaseModel):
    content: str


# --- APIエンドポイント ---

@app.get("/memos")
def get_memos():
    """メモを全件取得する"""
    conn = get_connection()
    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute("SELECT * FROM memos ORDER BY created_at DESC;")
            memos = cur.fetchall()
        return {"memos": memos}
    finally:
        conn.close()


@app.post("/memos")
def create_memo(memo: MemoCreate):
    """メモを1件登録する"""
    if not memo.content.strip():
        raise HTTPException(status_code=400, detail="メモの内容を入力してください")

    conn = get_connection()
    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute(
                "INSERT INTO memos (content) VALUES (%s) RETURNING *;",
                (memo.content,)
            )
            new_memo = cur.fetchone()
        conn.commit()
        return {"memo": new_memo}
    finally:
        conn.close()


@app.delete("/memos/{memo_id}")
def delete_memo(memo_id: int):
    """メモを1件削除する"""
    conn = get_connection()
    try:
        with conn.cursor() as cur:
            cur.execute("DELETE FROM memos WHERE id = %s;", (memo_id,))
            if cur.rowcount == 0:
                raise HTTPException(status_code=404, detail="指定したメモが見つかりません")
        conn.commit()
        return {"message": "削除しました"}
    finally:
        conn.close()

DB_CONFIGの password をSTEP1で設定したパスワードに変更してください。

作ったAPIの一覧

メソッド URL 処理
GET /memos メモを全件取得
POST /memos メモを1件登録
DELETE /memos/{id} メモを1件削除

PRスポンサーリンク
整体ショーツ(女性向け)

4. フロントエンドを更新する

index.html を更新する

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ひとことメモ</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <header>
    <h1>📝 ひとことメモ</h1>
  </header>

  <main>
    <!-- メモ入力フォーム -->
    <section class="form-section">
      <h2>メモを追加する</h2>
      <div class="input-area">
        <input type="text" id="memo-input" placeholder="メモを入力してください...">
        <button id="add-btn">保存する</button>
      </div>
      <p id="status-message"></p>
    </section>

    <!-- メモ一覧 -->
    <section class="list-section">
      <h2>保存済みのメモ</h2>
      <div id="memo-list">
        <p>読み込み中...</p>
      </div>
    </section>
  </main>

  <footer>
    <p>© 2026 ひとことメモ - WebアプリSTEP4</p>
  </footer>

  <script src="script.js"></script>
</body>
</html>

style.css を更新する

body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f5f5f5;
  color: #333;
}

header {
  background-color: #2ecc71;
  color: white;
  padding: 20px;
  text-align: center;
}

main {
  max-width: 640px;
  margin: 40px auto;
  padding: 0 16px;
}

section {
  background-color: white;
  border-radius: 8px;
  padding: 24px;
  margin-bottom: 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

h2 {
  color: #2ecc71;
  border-bottom: 2px solid #2ecc71;
  padding-bottom: 8px;
  margin-top: 0;
}

.input-area {
  display: flex;
  gap: 8px;
}

input[type="text"] {
  flex: 1;
  padding: 10px 14px;
  font-size: 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

button {
  background-color: #2ecc71;
  color: white;
  border: none;
  padding: 10px 20px;
  font-size: 16px;
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
}

button:hover {
  background-color: #27ae60;
}

#status-message {
  margin-top: 12px;
  font-size: 14px;
  color: #e44;
  min-height: 20px;
}

/* メモカード */
.memo-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  margin-bottom: 10px;
  background-color: #fafafa;
}

.memo-content {
  flex: 1;
  font-size: 16px;
}

.memo-date {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
}

.delete-btn {
  background-color: #e74c3c;
  padding: 6px 14px;
  font-size: 13px;
  margin-left: 12px;
}

.delete-btn:hover {
  background-color: #c0392b;
}

.empty-message {
  color: #999;
  text-align: center;
  padding: 20px 0;
}

footer {
  text-align: center;
  padding: 20px;
  color: #888;
  font-size: 14px;
}

script.js を更新する

const API_BASE = 'http://localhost:8000';

// --- DOM要素の取得 ---
const memoInput = document.getElementById('memo-input');
const addBtn = document.getElementById('add-btn');
const statusMessage = document.getElementById('status-message');
const memoList = document.getElementById('memo-list');

// --- メモ一覧を取得して表示する ---
async function loadMemos() {
  try {
    const response = await fetch(`${API_BASE}/memos`);
    const data = await response.json();

    if (data.memos.length === 0) {
      memoList.innerHTML = '<p class="empty-message">まだメモがありません。最初のメモを追加してみましょう!</p>';
      return;
    }

    // メモカードを生成して表示する
    memoList.innerHTML = data.memos.map(memo => `
      <div class="memo-card" id="memo-${memo.id}">
        <div>
          <div class="memo-content">${escapeHtml(memo.content)}</div>
          <div class="memo-date">${formatDate(memo.created_at)}</div>
        </div>
        <button class="delete-btn" onclick="deleteMemo(${memo.id})">削除</button>
      </div>
    `).join('');

  } catch (error) {
    memoList.innerHTML = '<p class="empty-message">サーバーに接続できませんでした。</p>';
  }
}

// --- メモを保存する ---
async function saveMemo() {
  const content = memoInput.value.trim();

  if (!content) {
    statusMessage.textContent = 'メモの内容を入力してください';
    return;
  }

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

    if (!response.ok) {
      throw new Error('保存に失敗しました');
    }

    // 入力欄をクリアしてメモ一覧を再読み込み
    memoInput.value = '';
    statusMessage.textContent = '保存しました!';
    setTimeout(() => { statusMessage.textContent = ''; }, 2000);
    loadMemos();

  } catch (error) {
    statusMessage.textContent = '保存に失敗しました';
  }
}

// --- メモを削除する ---
async function deleteMemo(id) {
  try {
    const response = await fetch(`${API_BASE}/memos/${id}`, {
      method: 'DELETE',
    });

    if (!response.ok) {
      throw new Error('削除に失敗しました');
    }

    loadMemos();

  } catch (error) {
    alert('削除に失敗しました');
  }
}

// --- ユーティリティ関数 ---

// XSS対策:HTMLの特殊文字をエスケープする
function escapeHtml(text) {
  const div = document.createElement('div');
  div.appendChild(document.createTextNode(text));
  return div.innerHTML;
}

// 日時を読みやすい形式にフォーマットする
function formatDate(dateString) {
  const date = new Date(dateString);
  return date.toLocaleString('ja-JP');
}

// --- イベントリスナー ---
addBtn.addEventListener('click', saveMemo);

// Enterキーでも保存できるようにする
memoInput.addEventListener('keydown', function(e) {
  if (e.key === 'Enter') saveMemo();
});

// ページ読み込み時にメモ一覧を取得する
loadMemos();

5. 動作確認

  1. サーバーを起動する(既に起動中であれば不要):
uvicorn main:app --reload
  1. ブラウザで index.html を開く

  2. テキストボックスにメモを入力して「保存する」ボタンを押す

  3. 一覧に追加されることを確認する

  4. ブラウザをリロードしても一覧が表示されることを確認する(データベースに保存されているため)

  5. 「削除」ボタンを押してメモが消えることを確認する


データの流れを整理しよう

メモを保存するときの流れ:

① ユーザーがボタンをクリック
        ↓
② JavaScriptが POST /memos をFastAPIに送信
        ↓
③ FastAPIがリクエストを受け取り、SQLを実行
        ↓
④ PostgreSQLがデータをmemosテーブルに保存
        ↓
⑤ FastAPIが保存されたデータをJSONで返す
        ↓
⑥ JavaScriptがメモ一覧を再読み込みして表示する

メモを取得するときの流れ:

① ページを開く(またはリロードする)
        ↓
② JavaScriptが GET /memos をFastAPIに送信
        ↓
③ FastAPIがSQLを実行
        ↓
④ PostgreSQLがmemosテーブルから全件取得
        ↓
⑤ FastAPIがデータをJSON配列で返す
        ↓
⑥ JavaScriptがHTMLを生成して一覧を表示する

SQLを直接確認してみよう

データが本当にデータベースに入っているか、psqlで確認できます:

psql -U postgres -d webapp_db
SELECT * FROM memos;
 id |    content     |         created_at
----+----------------+----------------------------
  1 | はじめてのメモ | 2026-05-22 10:30:00.123456
  2 | 2つ目のメモ   | 2026-05-22 10:31:15.654321
(2 rows)

プログラムから登録したデータがきちんと保存されていますね。


トラブルシューティング

エラー 原因 対処法
connection refused PostgreSQLが起動していない PostgreSQLサービスを起動する
password authentication failed パスワードが違う DB_CONFIG のpasswordを確認する
database "webapp_db" does not exist DBが作られていない psqlで CREATE DATABASE webapp_db; を実行
relation "memos" does not exist テーブルが作られていない psqlでテーブル作成SQLを実行する

まとめ:Webアプリの全体像

この連載を通じて、Webアプリの3層構造を体験しました:

【フロントエンド】
  HTML + CSS + JavaScript
  → 画面の表示・ユーザーの操作を担当

【バックエンド】
  Python(FastAPI)
  → ビジネスロジック・APIの処理を担当

【データベース】
  PostgreSQL
  → データの永続保存・管理を担当
STEP 学んだこと
STEP1 開発環境の構築
STEP2 HTML/CSS/JSでWebページを作る
STEP3 PythonでAPIサーバーを作りフロントと連携する
STEP4 PostgreSQLでデータを永続化する

次のステップ

この連載で基礎を学んだら、次はこんなことに挑戦してみましょう:

  • 認証機能を追加する — ログイン・ログアウト
  • デプロイする — 自分のWebアプリをインターネット上に公開する
  • Reactを学ぶ — より高度なフロントエンド開発

最初は難しく感じても、手を動かして作り続けることで必ず理解が深まります。まずは今回作ったアプリを自由に改造してみてください!


前の記事:【STEP3】バックエンドを理解する(Python)

PRスポンサーリンク
HitoHanaのお花の定期便