【初心者向け】Webアプリを作ろう STEP4:データベースを理解する(PostgreSQL)
いよいよ最終ステップです。
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件削除 |
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. 動作確認
- サーバーを起動する(既に起動中であれば不要):
uvicorn main:app --reload
-
ブラウザで
index.htmlを開く -
テキストボックスにメモを入力して「保存する」ボタンを押す
-
一覧に追加されることを確認する
-
ブラウザをリロードしても一覧が表示されることを確認する(データベースに保存されているため)
-
「削除」ボタンを押してメモが消えることを確認する
データの流れを整理しよう
メモを保存するときの流れ:
① ユーザーがボタンをクリック
↓
② 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を学ぶ — より高度なフロントエンド開発
最初は難しく感じても、手を動かして作り続けることで必ず理解が深まります。まずは今回作ったアプリを自由に改造してみてください!