第8部:実践プロジェクト Step 23 / 24

ブログアプリ実装編

設計に基づいて、Claude Codeと一緒にブログアプリを実装していきましょう。

各ステップでClaude Codeへの指示 → 生成結果の確認 → 動作確認の流れで進めます。

事前準備

プロジェクトフォルダの作成

まず、作業ディレクトリを作成してClaude Codeを起動します。

# プロジェクトフォルダを作成
mkdir blog-app
cd blog-app

# Claude Codeを起動
claude

Step 1: Docker環境のセットアップ

Claude Codeへの指示

ブログアプリ用のDocker Compose環境を作成してください。

構成:
- MySQL 8.0(ポート3306)
- FastAPI(Python 3.11、ポート8000)
- Next.js 14(ポート3000)

要件:
- 開発時のホットリロード対応
- MySQLデータの永続化(named volume)
- 環境変数は.envファイルで管理
- backendとfrontendそれぞれにDockerfileも作成
- .envファイルも作成(MySQL接続情報、SECRET_KEY)
- .gitignoreも作成

Claude Codeが行うこと

以下のファイルが生成されます。ファイル作成の許可を求められたら「Yes」を選択してください。

  • compose.yaml
  • backend/Dockerfile
  • backend/requirements.txt
  • frontend/Dockerfile
  • .env
  • .gitignore

完成コード: compose.yaml

# compose.yaml
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
    environment:
      DATABASE_URL: mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      SECRET_KEY: ${SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:8000
    depends_on:
      - backend

volumes:
  mysql_data:

完成コード: backend/Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

完成コード: .env

MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=blog
MYSQL_USER=bloguser
MYSQL_PASSWORD=blogpassword
SECRET_KEY=your-secret-key-change-in-production

動作確認

Claude Codeに以下を指示して、コンテナを起動します。

docker compose up -d を実行して、すべてのコンテナが起動することを確認してください。

確認ポイント:

  • docker compose ps で3つのコンテナがすべて「running」
  • http://localhost:8000 にアクセスできる
  • http://localhost:3000 にアクセスできる

よくあるエラーと対処法

ポートが既に使用されている

docker compose down で停止してから再起動。または他のアプリを停止。

MySQLの起動に時間がかかる

→ healthcheckの設定があるので、初回は1-2分待ってください。

backendがdbに接続できない

docker compose logs backend でログを確認。.envの値とcompose.yamlが一致しているか確認。

Step 2: バックエンド基盤

Claude Codeへの指示

FastAPIのバックエンド基盤を作成してください。

ファイル構成:
1. backend/main.py - FastAPIアプリのエントリポイント(CORS設定含む)
2. backend/config.py - pydantic-settingsで環境変数管理
3. backend/database.py - SQLAlchemy接続設定(セッション管理)
4. backend/models.py - User, Postモデル(SQLAlchemy)
5. backend/schemas.py - リクエスト/レスポンスのPydanticスキーマ

MySQL接続URL: 環境変数DATABASE_URLから取得
起動時にテーブルを自動作成する設定にしてください。

Claude Codeが行うこと

5つのPythonファイルが生成されます。それぞれ「Yes」で許可してください。

完成コード: backend/main.py

# backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from database import engine
from models import Base

app = FastAPI(title="Blog API")

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

# 起動時にテーブルを自動作成
@app.on_event("startup")
def startup():
    Base.metadata.create_all(bind=engine)

@app.get("/")
def root():
    return {"message": "Blog API is running"}

完成コード: backend/database.py

# backend/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from config import settings

engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# FastAPIの依存性注入で使うDB接続関数
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

完成コード: backend/models.py

# backend/models.py
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String(255), unique=True, index=True, nullable=False)
    name = Column(String(100), nullable=False)
    password_hash = Column(String(255), nullable=False)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

    posts = relationship("Post", back_populates="author")

class Post(Base):
    __tablename__ = "posts"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(255), nullable=False)
    content = Column(Text, nullable=False)
    author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

    author = relationship("User", back_populates="posts")

動作確認

docker compose restart backend を実行してバックエンドを再起動してください。
  • http://localhost:8000{"message": "Blog API is running"} が表示される
  • http://localhost:8000/docs でSwagger UIが表示される
  • docker compose logs backend にエラーがない

よくあるエラーと対処法

ModuleNotFoundError: No module named 'xxx'

→ requirements.txtにパッケージが不足しています。Claude Codeに「requirements.txtを確認して不足しているパッケージを追加してください」と指示。

sqlalchemy.exc.OperationalError: Can't connect to MySQL

→ MySQLコンテナが起動完了していない可能性。docker compose ps でdbのステータスを確認。

Step 3: 認証API実装

Claude Codeへの指示

JWT認証機能を実装してください。

ファイル:
1. backend/auth.py - パスワードハッシュ(bcrypt)、JWT生成・検証、get_current_user依存関数
2. backend/routers/auth.py - 認証エンドポイント

エンドポイント:
- POST /auth/register - ユーザー登録(email, password, name)
- POST /auth/login - ログイン(OAuth2PasswordRequestFormを使用、access_tokenを返す)
- GET /auth/me - 自分の情報取得(認証必要)

要件:
- bcryptでパスワードハッシュ
- JWTトークンの有効期限は30分
- SECRET_KEYは環境変数から取得
- email重複時は400エラー
- main.pyにルーターを登録

Claude Codeが行うこと

  • backend/auth.py を作成
  • backend/routers/auth.py を作成
  • backend/main.py にルーターを追加
  • backend/requirements.txt にパッケージ追加(python-jose, passlib, bcrypt)

完成コード: backend/auth.py

# backend/auth.py
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from database import get_db
from models import User
from config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=30)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, settings.secret_key, algorithm="HS256")

def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="認証情報が無効です",
    )
    try:
        payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
        user_id: int = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise credentials_exception
    return user

完成コード: backend/routers/auth.py

# backend/routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from database import get_db
from models import User
from schemas import UserCreate, UserResponse, Token
from auth import hash_password, verify_password, create_access_token, get_current_user

router = APIRouter(prefix="/auth", tags=["auth"])

@router.post("/register", response_model=UserResponse)
def register(user_data: UserCreate, db: Session = Depends(get_db)):
    # メール重複チェック
    existing = db.query(User).filter(User.email == user_data.email).first()
    if existing:
        raise HTTPException(status_code=400, detail="このメールアドレスは既に登録されています")

    user = User(
        email=user_data.email,
        name=user_data.name,
        password_hash=hash_password(user_data.password),
    )
    db.add(user)
    db.commit()
    db.refresh(user)
    return user

@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    user = db.query(User).filter(User.email == form_data.username).first()
    if not user or not verify_password(form_data.password, user.password_hash):
        raise HTTPException(status_code=401, detail="メールアドレスまたはパスワードが間違っています")

    access_token = create_access_token(data={"sub": user.id})
    return {"access_token": access_token, "token_type": "bearer"}

@router.get("/me", response_model=UserResponse)
def get_me(current_user: User = Depends(get_current_user)):
    return current_user

動作確認(Swagger UIでテスト)

http://localhost:8000/docs を開いて以下の順番でテストします。

  1. 1. ユーザー登録

    POST /auth/register を開き「Try it out」をクリック。以下を入力して「Execute」:

    {
      "email": "test@example.com",
      "password": "password123",
      "name": "テストユーザー"
    }

    → 200レスポンスでユーザー情報が返ればOK

  2. 2. ログイン

    POST /auth/login を開き「Try it out」をクリック。usernameに test@example.com、passwordに password123 を入力して「Execute」。

    access_token が返ればOK。このトークンをコピーしておく。

  3. 3. 認証テスト

    ページ上部の「Authorize」ボタンをクリック → コピーしたトークンを貼り付けて「Authorize」。

    GET /auth/me を開き「Try it out」→「Execute」。

    → 自分のユーザー情報が返ればOK

Step 4: 記事API実装

Claude Codeへの指示

記事のCRUD APIを実装してください。

ファイル:
- backend/routers/posts.py

エンドポイント:
- GET /posts - 記事一覧(ページネーション: skip, limit パラメータ)
- GET /posts/{id} - 記事詳細(著者情報も含む)
- POST /posts - 記事作成(認証必要)
- PUT /posts/{id} - 記事更新(本人のみ)
- DELETE /posts/{id} - 記事削除(本人のみ)

要件:
- 一覧は新しい順(created_at DESC)にソート
- 詳細では著者のnameとemailも返す
- 本人以外の更新・削除は403エラー
- 存在しない記事IDは404エラー
- schemas.pyにPostCreate, PostUpdate, PostResponseスキーマを追加
- main.pyにルーターを登録

完成コード: backend/routers/posts.py

# backend/routers/posts.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from database import get_db
from models import Post, User
from schemas import PostCreate, PostUpdate, PostResponse
from auth import get_current_user

router = APIRouter(prefix="/posts", tags=["posts"])

@router.get("/", response_model=List[PostResponse])
def get_posts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    posts = db.query(Post).order_by(Post.created_at.desc()).offset(skip).limit(limit).all()
    return posts

@router.get("/{post_id}", response_model=PostResponse)
def get_post(post_id: int, db: Session = Depends(get_db)):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="記事が見つかりません")
    return post

@router.post("/", response_model=PostResponse, status_code=201)
def create_post(
    post_data: PostCreate,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    post = Post(**post_data.dict(), author_id=current_user.id)
    db.add(post)
    db.commit()
    db.refresh(post)
    return post

@router.put("/{post_id}", response_model=PostResponse)
def update_post(
    post_id: int,
    post_data: PostUpdate,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="記事が見つかりません")
    if post.author_id != current_user.id:
        raise HTTPException(status_code=403, detail="この記事を編集する権限がありません")

    for key, value in post_data.dict(exclude_unset=True).items():
        setattr(post, key, value)
    db.commit()
    db.refresh(post)
    return post

@router.delete("/{post_id}", status_code=204)
def delete_post(
    post_id: int,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="記事が見つかりません")
    if post.author_id != current_user.id:
        raise HTTPException(status_code=403, detail="この記事を削除する権限がありません")

    db.delete(post)
    db.commit()

動作確認(Swagger UI + curl)

Swagger UIまたはcurlで以下を順番にテストします。

  1. 1. 記事作成(認証済みの状態で)

    curl -X POST http://localhost:8000/posts/ \
      -H "Authorization: Bearer YOUR_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"title": "最初の記事", "content": "Hello World! これはテスト記事です。"}'
  2. 2. 記事一覧取得

    curl http://localhost:8000/posts/

    → 作成した記事が配列で返ればOK

  3. 3. 他人の記事を編集しようとする(403エラーの確認)

    → 別ユーザーで登録・ログインし、最初のユーザーの記事IDでPUTリクエスト → 403エラーが返ればOK

Step 5: フロントエンド実装

フロントエンドは3回に分けてClaude Codeに指示します。

Claude Codeへの指示(1/3:認証機能)

Next.jsのフロントエンドで認証機能を実装してください。

ファイル:
1. frontend/src/contexts/AuthContext.tsx - 認証状態管理(Context API)
2. frontend/src/app/login/page.tsx - ログインページ
3. frontend/src/app/register/page.tsx - 新規登録ページ
4. frontend/src/components/Header.tsx - ナビゲーション(認証状態で表示切替)
5. frontend/src/components/RequireAuth.tsx - 認証必須ラッパー
6. frontend/src/lib/api.ts - APIクライアント(fetch wrapper)

要件:
- トークンはlocalStorageに保存
- ログイン状態はContext APIで管理
- ログアウト後はトップページにリダイレクト
- APIのベースURLは環境変数NEXT_PUBLIC_API_URLから取得
- ログイン/登録フォームにバリデーションエラー表示

完成コード: frontend/src/contexts/AuthContext.tsx(主要部分)

// frontend/src/contexts/AuthContext.tsx
"use client";
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { api } from "@/lib/api";

type User = { id: number; email: string; name: string };

type AuthContextType = {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => void;
  isLoading: boolean;
};

const AuthContext = createContext<AuthContextType>({} as AuthContextType);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // ページ読み込み時にトークンがあればユーザー情報を取得
    const token = localStorage.getItem("token");
    if (token) {
      api.get("/auth/me")
        .then(setUser)
        .catch(() => localStorage.removeItem("token"))
        .finally(() => setIsLoading(false));
    } else {
      setIsLoading(false);
    }
  }, []);

  const login = async (email: string, password: string) => {
    const formData = new URLSearchParams();
    formData.append("username", email);
    formData.append("password", password);
    const data = await api.postForm("/auth/login", formData);
    localStorage.setItem("token", data.access_token);
    const userData = await api.get("/auth/me");
    setUser(userData);
  };

  const logout = () => {
    localStorage.removeItem("token");
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, register, logout, isLoading }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

完成コード: frontend/src/components/Header.tsx(主要部分)

// frontend/src/components/Header.tsx
"use client";
import Link from "next/link";
import { useAuth } from "@/contexts/AuthContext";

export default function Header() {
  const { user, logout } = useAuth();

  return (
    <header className="bg-white shadow">
      <nav className="container mx-auto px-6 py-4 flex justify-between items-center">
        <Link href="/" className="text-xl font-bold">Blog App</Link>
        <div className="flex items-center gap-4">
          {user ? (
            <>
              <Link href="/posts/new" className="bg-blue-600 text-white px-4 py-2 rounded">
                記事を書く
              </Link>
              <span>{user.name}</span>
              <button onClick={logout} className="text-gray-600">ログアウト</button>
            </>
          ) : (
            <>
              <Link href="/login">ログイン</Link>
              <Link href="/register" className="bg-blue-600 text-white px-4 py-2 rounded">
                新規登録
              </Link>
            </>
          )}
        </div>
      </nav>
    </header>
  );
}

Claude Codeへの指示(2/3:記事表示)

記事の表示機能を実装してください。

ファイル:
1. frontend/src/app/page.tsx - トップページ(記事一覧)
2. frontend/src/app/posts/[id]/page.tsx - 記事詳細ページ
3. frontend/src/components/ArticleCard.tsx - 記事カードコンポーネント

要件:
- トップページに記事一覧をカード形式で表示
- 各カードにはタイトル、著者名、作成日を表示
- クリックで記事詳細ページに遷移
- 詳細ページでは本人なら「編集」「削除」ボタンを表示
- 記事がない場合は「記事がありません」と表示
- 作成日はyyyy/mm/dd形式で表示

Claude Codeへの指示(3/3:記事作成・編集)

記事の作成・編集機能を実装してください。

ファイル:
1. frontend/src/app/posts/new/page.tsx - 記事作成ページ
2. frontend/src/app/posts/[id]/edit/page.tsx - 記事編集ページ
3. frontend/src/components/PostForm.tsx - 記事フォーム(作成・編集共通)

要件:
- 認証必須(RequireAuthで囲む)
- バリデーション(タイトル必須、本文10文字以上)
- 送信後は記事詳細にリダイレクト
- 編集ページは既存データを初期値として読み込む
- 削除は確認ダイアログを表示してから実行
- 送信中はボタンを無効化してローディング表示

完成コード: frontend/src/components/PostForm.tsx(主要部分)

// frontend/src/components/PostForm.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";

type Props = {
  initialData?: { title: string; content: string };
  postId?: number;
};

export default function PostForm({ initialData, postId }: Props) {
  const router = useRouter();
  const [title, setTitle] = useState(initialData?.title ?? "");
  const [content, setContent] = useState(initialData?.content ?? "");
  const [errors, setErrors] = useState<string[]>([]);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const isEdit = !!postId;

  const validate = () => {
    const newErrors: string[] = [];
    if (!title.trim()) newErrors.push("タイトルは必須です");
    if (content.length < 10) newErrors.push("本文は10文字以上必要です");
    setErrors(newErrors);
    return newErrors.length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validate()) return;
    setIsSubmitting(true);

    try {
      const data = { title, content };
      if (isEdit) {
        await api.put(`/posts/${postId}`, data);
        router.push(`/posts/${postId}`);
      } else {
        const newPost = await api.post("/posts/", data);
        router.push(`/posts/${newPost.id}`);
      }
    } catch (err) {
      setErrors(["保存に失敗しました"]);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {errors.map((err, i) => (
        <p key={i} className="text-red-600 text-sm">{err}</p>
      ))}
      <input value={title} onChange={(e) => setTitle(e.target.value)}
        placeholder="タイトル" className="w-full p-3 border rounded" />
      <textarea value={content} onChange={(e) => setContent(e.target.value)}
        placeholder="本文" rows={10} className="w-full p-3 border rounded" />
      <button type="submit" disabled={isSubmitting}
        className="bg-blue-600 text-white px-6 py-3 rounded disabled:opacity-50">
        {isSubmitting ? "保存中..." : isEdit ? "更新する" : "投稿する"}
      </button>
    </form>
  );
}

動作確認(ブラウザで確認)

http://localhost:3000 を開いて以下を順番にテストします。

  1. ☑ トップページに記事一覧が表示される(まだ記事がなければ空でOK)
  2. ☑ 「新規登録」→ フォームに入力 → 登録成功 → ログイン状態になる
  3. ☑ ヘッダーに「記事を書く」ボタンとユーザー名が表示される
  4. ☑ 「記事を書く」→ タイトルと本文を入力 → 「投稿する」→ 記事詳細に遷移
  5. ☑ トップページに戻ると記事が表示されている
  6. ☑ 記事詳細で「編集」→ 内容を変更 → 「更新する」→ 変更が反映
  7. ☑ 記事詳細で「削除」→ 確認ダイアログで「OK」→ トップページに遷移
  8. ☑ ログアウト → 「記事を書く」ボタンが消える

よくあるエラーと対処法

CORSエラー(ブラウザのコンソールに赤いエラー)

→ backend/main.pyのCORS設定で http://localhost:3000 が許可されているか確認。

ログイン時に「Network Error」

→ backendコンテナが起動しているか docker compose ps で確認。NEXT_PUBLIC_API_URL が正しく設定されているか確認。

ページ遷移後に画面が真っ白になる

→ ブラウザのDevTools(F12)→ Consoleタブでエラーを確認。エラーメッセージをそのままClaude Codeに貼り付けて質問。

記事投稿時に401エラー

→ APIクライアント(lib/api.ts)でAuthorizationヘッダーにトークンを付与しているか確認。

デバッグのコツ

エラーが出たら

エラーメッセージをそのままClaude Codeに貼り付けてください。

以下のエラーが出ました。原因と修正方法を教えてください。

[エラーメッセージをここに貼り付け]

バックエンドのログ確認

Claude Codeに以下を指示するとログを確認できます。

docker compose logs -f backend

フロントエンドのデバッグ

ブラウザのDevTools(F12キー)を開き:

  • Consoleタブ → JavaScriptエラーの確認
  • Networkタブ → APIリクエストのステータスコードとレスポンスの確認

データベースの確認

# MySQLに接続してデータを確認
docker compose exec db mysql -u bloguser -pblogpassword blog
> SELECT * FROM users;
> SELECT * FROM posts;
設計編 次へ:仕上げ編