404 Not Found

404 Not Found


nginx

総合演習 — コア機能

第2フェーズでは, Pinia, Composable, Server API, Middleware, Pluginsを学びました。今こそそれらをすべて結びつける時です。Charlieの要件は:カートはAPIからStore, ページまで完全に統合されること。Bobの管理画面にはアクセス制御があること。Aliceのショッピング体験はシームレスで途切れないこと。

1. 学ぶ内容


2. チームのある本当の話

(1) 課題:モジュールは独立して動くが統合できない

Aliceのカートページは商品を表示しますが, リフレッシュするとデータが消えます。BobのAPIは書かれているのに, フロントエンドが呼び出していません。ミドルウェアは書かれているのに統合されていません。各モジュールは単体では動作しますが, 組み合わせるとシステムが壊れます。

(2) エンドツーエンド統合テストの解決策

API → Composable → Store → ページのデータチェーンを完全に統合し, 各段階でデータフローを明確にします。

(3) メリット:完全なショッピング体験

Aliceはカートに商品を追加 → リフレッシュ後も商品が保持される → 注文ページに進む → 管理画面はアクセス制御で保護される。全プロセスがシームレスです。


3. 第2フェーズコア機能モジュールの連携アーキテクチャ

100%
graph TB
    A[MegaShop 第2フェーズ] --> B[Server API層]
    A --> C[Composable層]
    A --> D[状態層 - Pinia]
    A --> E[ミドルウェア層]
    A --> F[プラグイン層]

    B --> B1[/api/products CRUD]
    B --> B2[/api/cart CRUD]
    B --> B3[/api/auth ログイン/登録]

    C --> C1[useCart → Store + API]
    C --> C2[useProductFilter → API]
    C --> C3[usePriceFormat → 純粋関数]

    D --> D1[cartStore → items/total]
    D --> D2[userStore → auth/profile]

    E --> E1[auth.global.ts → ログインチェック]
    E --> E2[admin.ts → ロールチェック]

    F --> F1[logger → デバッグ]
    F --> F2[stripe.client → 決済]

    C1 --> D1
    C1 --> B2
    C2 --> B1

(1) 完全なデータフロー

操作 データフロー 関連モジュール
商品閲覧 API → useFetch → ページ Server API
カートに追加 API → useCart → cartStore → ページ API + Composable + Store
カート表示 cartStore → ページ Store
注文 cartStore → useCheckout → Stripe → API Store + Composable + Plugin + API
管理画面 authミドルウェア → adminミドルウェア → ページ Middleware

4. 完全なカートAPI

(1) ▶ サンプル:完全なカートAPI

TYPESCRIPT
// server/api/cart/index.get.ts
export default defineEventHandler((event) => {
  const sessionId = getCookie(event, 'session-id') || 'default'
  const cart = mockCarts.find(c => c.sessionId === sessionId)
  return cart || { sessionId, items: [], total: 0 }
})

出力:

TEXT
// 実行成功
TYPESCRIPT
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
  const sessionId = getCookie(event, 'session-id') || 'default'
  const { productId, quantity = 1 } = await readBody(event)

  const product = mockProducts.find(p => p.id === productId)
  if (!product) throw createError({ statusCode: 404, message: 'Product not found' })

  let cart = mockCarts.find(c => c.sessionId === sessionId)
  if (!cart) {
    cart = { sessionId, items: [], total: 0 }
    mockCarts.push(cart)
  }

  const existing = cart.items.find(i => i.productId === productId)
  if (existing) {
    existing.quantity += quantity
  } else {
    cart.items.push({ productId, name: product.name, price: product.price, quantity, image: product.image })
  }
  cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
  setCookie(event, 'session-id', sessionId, { httpOnly: true, maxAge: 86400 * 30 })
  return cart
})
TYPESCRIPT
// server/api/cart/remove.delete.ts
export default defineEventHandler(async (event) => {
  const sessionId = getCookie(event, 'session-id') || 'default'
  const { productId } = await readBody(event)

  const cart = mockCarts.find(c => c.sessionId === sessionId)
  if (!cart) throw createError({ statusCode: 404, message: 'Cart not found' })

  cart.items = cart.items.filter(i => i.productId !== productId)
  cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
  return cart
})

5. Pinia Store + Composable統合

(1) ▶ サンプル:強化版cartStore

TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const isLoading = ref(false)

  const totalItems = computed(() => items.value.reduce((s, i) => s + i.quantity, 0))
  const totalPrice = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
  const formattedTotal = computed(() =>
    new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(totalPrice.value)
  )

  async function fetchCart() {
    isLoading.value = true
    try {
      const cart = await $fetch('/api/cart')
      items.value = cart.items || []
    } finally {
      isLoading.value = false
    }
  }

  async function addItem(productId: number, quantity = 1) {
    const cart = await $fetch('/api/cart/add', {
      method: 'POST', body: { productId, quantity }
    })
    items.value = cart.items
  }

  async function removeItem(productId: number) {
    const cart = await $fetch('/api/cart/remove', {
      method: 'DELETE', body: { productId }
    })
    items.value = cart.items
  }

  function clearCart() {
    items.value = []
  }

  return { items, isLoading, totalItems, totalPrice, formattedTotal, fetchCart, addItem, removeItem, clearCart }
})

interface CartItem { productId: number; name: string; price: number; quantity: number; image: string }

出力:

TEXT
// 実行成功

(2) ▶ サンプル:useCart Composable

TYPESCRIPT
// composables/useCart.ts
export function useCart() {
  const cartStore = useCartStore()
  const { $logger } = useNuxtApp()
  const { notify } = useNotification()

  async function addToCart(productId: number, productName: string) {
    try {
      await cartStore.addItem(productId)
      notify(`${productName} added to cart`, 'success')
      $logger.info('Item added to cart', { productId })
    } catch (err) {
      notify('Failed to add item', 'error')
      $logger.error('Add to cart failed', { productId, err })
    }
  }

  async function removeFromCart(productId: number) {
    try {
      await cartStore.removeItem(productId)
      notify('Item removed from cart', 'info')
    } catch (err) {
      notify('Failed to remove item', 'error')
    }
  }

  return {
    items: computed(() => cartStore.items),
    totalItems: computed(() => cartStore.totalItems),
    totalPrice: computed(() => cartStore.totalPrice),
    formattedTotal: computed(() => cartStore.formattedTotal),
    isLoading: computed(() => cartStore.isLoading),
    addToCart,
    removeFromCart
  }
}

出力:

TEXT
// 実行成功

6. 権限ミドルウェア設定

(1) ▶ サンプル:完全なミドルウェアアーキテクチャ

TYPESCRIPT
// middleware/01-auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const publicPaths = ['/', '/products', '/about', '/login', '/register']
  if (publicPaths.some(p => to.path === p || to.path.startsWith('/products'))) return

  const token = useCookie('auth-token')
  if (!token.value) return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
})

出力:

TEXT
// 実行成功
TYPESCRIPT
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
  const userCookie = useCookie('user-data')
  if (!userCookie.value) return navigateTo('/login')
  if ((userCookie.value as any).role !== 'admin') {
    throw createError({ statusCode: 403, message: 'Admin access required' })
  }
})

7. エンドツーエンド統合テストページ

(1) ▶ サンプル:商品詳細ページ (完全データフロー)

VUE
<!-- pages/products/[id].vue -->
<template>
  <div v-if="product" class="product-detail">
    <img :src="product.image" :alt="product.name" />
    <div class="info">
      <h1>{{ product.name }}</h1>
      <p class="price">{{ formattedPrice }}</p>
      <p>{{ product.reviewCount }} reviews</p>
      <button @click="handleAddToCart" :disabled="adding">
        {{ adding ? 'Adding...' : 'Add to Cart' }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
const { formatted } = usePriceFormat(computed(() => product.value?.price || 0))
const formattedPrice = formatted

const { addToCart } = useCart()
const adding = ref(false)

async function handleAddToCart() {
  if (!product.value) return
  adding.value = true
  await addToCart(product.value.id, product.value.name)
  adding.value = false
}
</script>

出力:

TEXT
// 実行成功

8. 統合テストチェックリスト

テスト項目 検証方法 期待結果
APIが利用可能 curl /api/products 商品JSONが返される
Store状態共有 ホームページで追加 → 詳細ページで数量確認 数量が一致
Composable呼び出し 「カートに追加」をクリック 成功:追加 + 通知
ミドルウェア保護 未認証で/adminにアクセス /loginにリダイレクト
プラグイン実行 コンソールを確認 Logger出力あり
SSRデータハイドレーション ソースコードを表示 HTMLに商品データが含まれる
Cookie永続化 ページをリフレッシュ カートデータが保持される

❓ よくある質問

Q StoreとAPIのデータが同期しない場合はどうすればよいですか?
A StoreがaddItemでAPIを呼び出した後, 戻り値を使ってitems配列を更新し, Storeとバックエンドの同期を確保してください。APIを呼び出さずにStoreだけを変更しないでください。
Q ComposableでStoreと$fetchの両方を使うのは冗長ですか?
A いいえ。Storeはクライアント側の状態を管理し, $fetchはバックエンドと同期します。useCartは両方をラップしているため, 呼び出し側は詳細を気にする必要がありません。
Q ページミドルウェアとAPIミドルウェアの両方が認証を処理すると競合しませんか?
A いいえ。ページミドルウェアはルート遷移を保護し (フロントエンド), サーバーミドルウェアはAPIリクエストを保護します (バックエンド)。両者は補完関係にあり, どちらも必要です。
Q エンドツーエンドのデータフローをデバッグするにはどうすればよいですか?
A Nuxt DevToolsのPiniaパネルでStore状態を確認し, NetworkパネルでAPIリクエストを確認し, コンソールでlogger出力を確認します。APIレスポンス → Store更新 → ページレンダリングの順に段階的にトラブルシューティングしてください。
Q SSR時にカートデータはどのように初期化されますか?
A SSR時, CookieからセッションIDを取得し, APIがセッションIDに基づいてカートを返します。クライアント側のハイドレーション完了後, StoreはAPIペイロードからデータを取得します。
Q 第2フェーズのモックデータはいつ本物のデータベースに置き換えられますか?
A 第4フェーズのレッスン20で, Prismaに置き換えます。APIインターフェース形式はすでに設計済みで, server APIの内部実装を変更するだけです。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度:⭐):カートAPIとcartStoreの統合を完了し, 商品詳細ページで「カートに追加」ボタンをクリックした際にカート数が更新されるようにしてください。
  2. 応用問題 (難易度:⭐⭐):完全なミドルウェアシステムを実装:未認証ユーザーが/checkoutにアクセスしたらログインページにリダイレクト, 一般ユーザーが/adminにアクセスしたら403エラーを返す。
  3. チャレンジ (難易度:⭐⭐⭐):エンドツーエンドのショッピングフローを実装:商品閲覧 → カートに追加 → カート表示 → 注文 (Stripeプラグインで決済をシミュレート)。全プロセスでデータの一貫性を確保してください。

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%