プロジェクト開発
設計が完了し, Charlieはチームを率いてコーディングを開始しました。まず認証モジュールが立ち上がりました - ログインなしではアクセスできません。次に商品モジュール - 数百万商品のCRUD操作とISRキャッシュ。ショッピングカートと注文モジュールが続きました。Aliceは顧客フローをテストし, Bobは管理機能を検証しました。各モジュールはエンドツーエンドテストを受けました。
1. 学ぶ内容
- 認証モジュール:OAuth2 + JWT + 認証ミドルウェア + ロールベース権限
- 商品モジュール:数百万CRUD + ISRキャッシュ + SEO + 検索とフィルタ
- ショッピングカートモジュール:Pinia Store + Composable + サーバーAPI + 在庫検証
- 注文モジュール:Prismaトランザクション + ステートマシン + Stripe決済
- 国際化とパフォーマンス:3言語i18n + @nuxt/image + Lighthouse監査
2. チームのリアルストーリー
(1) ペインポイント:設計図が実装できない
Charlieには完全な設計ドキュメントがありますが, チームは何から手を付ければよいか分かっていません。Aliceは商品ページを書きましたが, ショッピングカートのAPIがまだありません。Bobは管理パネルを書きましたが, 認証がまだ設定されていません。モジュール同士が互いを待ち合い, 進捗が止まっています。
(2) モジュール別段階的開発のソリューション
依存関係の順にモジュールを開発:認証 → 商品 → ショッピングカート → 注文 → 国際化。各モジュールは完成直後にテストし, まだ完成していないモジュールに依存しないようにします。
(3) 成果:着実な進捗
各モジュールは3〜5日で完了し, 2週間以内にすべてのコア機能が立ち上がります。その時点でAliceは全ショッピングフローを完了できるようになります。
3. 開発マイルストーン
(1) モジュール開発ガントチャート
gantt
title MegaShop開発マイルストーン
dateFormat YYYY-MM-DD
section 認証モジュール
JWT + OAuth2 :a1, 2026-01-01, 3d
認証ミドルウェア :a2, after a1, 2d
section 商品モジュール
CRUD API :p1, after a2, 3d
検索 + フィルタ :p2, after p1, 2d
ISRキャッシュ :p3, after p2, 1d
section カートモジュール
Store + API :c1, after a2, 3d
Composable :c2, after c1, 1d
section 注文モジュール
Prismaトランザクション :o1, after c1, 3d
Stripe決済 :o2, after o1, 2d
section i18n + パフォーマンス
3言語対応 :i1, after p3, 2d
画像最適化 :i2, after i1, 1d
Lighthouse監査 :i3, after i2, 1d
4. 認証モジュール
(1) ▶サンプル:JWTユーティリティ + 登録/ログインAPI
TYPESCRIPT
// server/utils/jwt.ts
import jwt from 'jsonwebtoken'
const config = useRuntimeConfig()
export function signTokenPair(payload: { userId: number; email: string; role: string }) {
return {
accessToken: jwt.sign(payload, config.jwtAccessSecret, { expiresIn: '15m' }),
refreshToken: jwt.sign(payload, config.jwtRefreshSecret, { expiresIn: '7d' })
}
}
export function setAuthCookies(event: any, tokens: { accessToken: string; refreshToken: string }) {
setCookie(event, 'access-token', tokens.accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 })
setCookie(event, 'refresh-token', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 604800 })
}
出力:
TEXT
// 実行成功
TYPESCRIPT
// server/api/auth/register.post.ts
export default defineEventHandler(async (event) => {
const { email, password, name } = await readBody(event)
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) throw createError({ statusCode: 409, message: 'Email already registered' })
const hashedPassword = await hashPassword(password)
const user = await prisma.user.create({
data: { email, name, password: hashedPassword, role: 'CUSTOMER' }
})
const tokens = signTokenPair({ userId: user.id, email: user.email, role: user.role })
setAuthCookies(event, tokens)
return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})
5. 商品モジュール
(1) ▶サンプル:商品CRUD API (Prisma + キャッシュ)
TYPESCRIPT
// server/api/products/index.get.ts
export default cachedEventHandler(
async (event) => {
const { page = '1', limit = '20', category, search, sort } = getQuery(event)
const where = {
AND: [
category ? { category: { slug: category as string } } : {},
search ? { OR: [
{ name: { contains: search as string, mode: 'insensitive' } },
{ description: { contains: search as string, mode: 'insensitive' } }
]} : {}
]
}
const orderBy = sort === 'price-asc' ? { price: 'asc' }
: sort === 'price-desc' ? { price: 'desc' }
: { createdAt: 'desc' }
const [items, total] = await Promise.all([
prisma.product.findMany({
where, orderBy,
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
include: { category: { select: { name: true, slug: true } } }
}),
prisma.product.count({ where })
])
return { items, total, page: Number(page), limit: Number(limit) }
},
{ maxAge: 60, swr: true, getKey: (event) => `products:${getRequestURL(event).search}` }
)
出力:
TEXT
// 実行成功
(2) ▶サンプル:商品詳細 + SEO
VUE
<!-- pages/products/[id].vue -->
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<p>{{ formatFromUSD(product.price) }}</p>
<button @click="handleAddToCart">{{ t('product.addToCart') }}</button>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const config = useRuntimeConfig()
const { t } = useI18n()
const { formatFromUSD } = useLocalizedPrice()
const { addToCart } = useCart()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
// 商品ごとの動的SEO
useSeoMeta({
title: () => `${product.value?.name} - MegaShop`,
description: () => `Buy ${product.value?.name} - $${product.value?.price} USD`,
ogTitle: () => product.value?.name,
ogImage: () => product.value?.image,
ogType: 'product'
})
// JSON-LD構造化データ
useHead({ script: [{ type: 'application/ld+json', innerHTML: computed(() => JSON.stringify({
'@context': 'https://schema.org', '@type': 'Product',
name: product.value?.name,
offers: { '@type': 'Offer', priceCurrency: 'USD', price: product.value?.price }
}))}]})
async function handleAddToCart() {
if (product.value) await addToCart(product.value.id, product.value.name)
}
</script>
出力:
TEXT
// 実行成功
6. ショッピングカートモジュール
(1) ▶サンプル:cartStore + useCart
TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
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))
async function addItem(productId: number) {
const cart = await $fetch('/api/cart/add', { method: 'POST', body: { productId } })
items.value = cart.items
}
async function removeItem(productId: number) {
const cart = await $fetch('/api/cart/remove', { method: 'DELETE', body: { productId } })
items.value = cart.items
}
async function fetchCart() {
const cart = await $fetch('/api/cart')
items.value = cart.items || []
}
return { items, totalItems, totalPrice, addItem, removeItem, fetchCart }
})
出力:
TEXT
// 実行成功
(2) ▶サンプル:ショッピングカートAPI (在庫検証付き)
TYPESCRIPT
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { productId, quantity = 1 } = await readBody(event)
// 在庫の確認
const product = await prisma.product.findUnique({ where: { id: productId } })
if (!product?.inStock) throw createError({ statusCode: 400, message: 'Product out of stock' })
// カートアイテムのアップサート
await prisma.cartItem.upsert({
where: { userId_productId: { userId, productId } },
update: { quantity: { increment: quantity } },
create: { userId, productId, quantity }
})
const cart = await prisma.cartItem.findMany({
where: { userId },
include: { product: { select: { name: true, price: true, image: true } } }
})
const total = cart.reduce((s, i) => s + Number(i.product.price) * i.quantity, 0)
return { items: cart, total }
})
出力:
TEXT
// 実行成功
7. 注文モジュール
(1) ▶サンプル:注文作成トランザクション
TYPESCRIPT
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
if (!userId) throw createError({ statusCode: 401, message: 'Login required' })
const order = await prisma.$transaction(async (tx) => {
// カートアイテムの取得
const cartItems = await tx.cartItem.findMany({
where: { userId },
include: { product: true }
})
if (cartItems.length === 0) {
throw createError({ statusCode: 400, message: 'Cart is empty' })
}
// すべてのアイテムの在庫を確認
for (const item of cartItems) {
if (!item.product.inStock) {
throw createError({ statusCode: 400, message: `${item.product.name} out of stock` })
}
}
// 合計金額の計算
const total = cartItems.reduce((sum, item) => sum + Number(item.product.price) * item.quantity, 0)
// アイテム付きで注文を作成
const newOrder = await tx.order.create({
data: {
userId,
total,
status: 'PENDING',
items: {
create: cartItems.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.product.price
}))
}
},
include: { items: { include: { product: true } } }
})
// カートをクリア
await tx.cartItem.deleteMany({ where: { userId } })
return newOrder
})
return { order, message: 'Order created' }
})
出力:
TEXT
// 実行成功
8. 国際化とパフォーマンス
(1) ▶サンプル:nuxt.config.ts完全本番設定
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
'@nuxtjs/i18n',
'@nuxtjs/sitemap',
'@nuxt/image',
'~/modules/analytics'
],
app: {
head: {
title: 'MegaShop',
titleTemplate: '%s | MegaShop',
meta: [
{ name: 'description', content: 'Over 1 million products shipped worldwide' }
]
}
},
i18n: {
locales: [
{ code: 'en', name: 'English', file: 'en.json', currency: 'USD' },
{ code: 'zh', name: 'Chinese', file: 'zh.json', currency: 'CNY' },
{ code: 'ja', name: 'Japanese', file: 'ja.json', currency: 'JPY' }
],
defaultLocale: 'en',
lazy: true,
langDir: 'locales/',
strategy: 'prefix_except_default'
},
image: {
quality: 80,
format: ['webp', 'avif']
},
sitemap: {
hostname: 'https://megashop.com',
gzip: true
},
runtimeConfig: {
databaseUrl: process.env.DATABASE_URL,
redisUrl: process.env.REDIS_URL,
jwtAccessSecret: process.env.JWT_ACCESS_SECRET,
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET,
public: {
apiBase: process.env.API_BASE || '/api',
siteUrl: process.env.SITE_URL || 'https://megashop.com',
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY
}
},
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/products': { swr: 3600 },
'/products/**': { swr: 86400 },
'/categories/**': { swr: 21600 },
'/admin/**': { ssr: false },
'/cart': { ssr: false },
'/checkout': { ssr: false },
'/profile/**': { ssr: false },
'/api/**': { cors: true },
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } }
},
nitro: {
preset: process.env.DEPLOY_TARGET || 'node-server',
storage: {
products: { driver: process.env.NODE_ENV === 'production' ? 'redis' : 'memory' }
}
},
components: [{ path: '~/components', pathPrefix: false }]
})
出力:
TEXT
// 実行成功
❓よくある質問
Q モジュールの開発順序は重要ですか?
A 非常に重要です。認証を最初に実装する必要があります (他のモジュールがログイン状態に依存するため), 次に商品 (ショッピングカートと注文が商品データに依存するため), 最後に国際化 (コア機能に影響しないため)です。
Q 各モジュールはおおよそどのくらいのコード量ですか?
A 認証モジュール約500行, 商品モジュール約800行, ショッピングカート約400行, 注文約500行, 国際化約200行。設定とテストを含めると, 合計約3,000〜4,000行です。
Q モジュール間のAPIコントラクトはどのように確保しますか?
A TypeScriptインターフェースでAPIのリクエストとレスポンスの型を定義し,
shared/types.tsに配置してください。フロントエンドとバックエンドでこれらの型定義を共有し, コンパイル時に一致性をチェックします。Q 開発中にモックデータからPrismaへはどのように移行しますか?
A APIインターフェースの形式は変更せず, 内部実装のみを置き換えます。モック配列から
prisma.findMany()に切り替え, ルートとレスポンス形式はそのままにします。Q Stripe決済のテストはどうすればよいですか?
A Stripeのテスト用シークレットとテストカード番号 (4242 4242 4242 4242)を使用してください。実際の課金は発生しません。本番運用前に本番用シークレットに切り替えてください。
Q i18nの翻訳はいつ行うべきですか?
A コア機能が完成した後に行ってください。まず英語のテキストをハードコードし, 機能が安定したら翻訳キーとして抽出します。早すぎるi18nの導入は開発の複雑さを増加させます。
📖まとめ
- 依存関係の順に開発:認証 → 商品 → ショッピングカート → 注文 → 国際化
- 認証モジュール:JWTデュアルトークン + httpOnlyクッキー + 認証ミドルウェア + 管理者ロールチェック
- 商品モジュール:Prisma CRUD + Nitroキャッシュ + ISR + SEOメタタグ + JSON-LD
- ショッピングカート:Pinia Store + useCart Composable + 在庫検証
- 注文:Prismaトランザクションが原子性を保証 + Stripe決済統合
📝練習問題
- 基本問題 (難易度:⭐):認証モジュール (登録/ログイン/リフレッシュトークン)を実装し, クッキーが正しく設定されることを確認してください
- 応用問題 (難易度:⭐⭐):ショッピングカートとチェックアウトを含む完全な商品CRUDワークフローを実装し, エンドツーエンドの動作するアプリケーションを完成させてください
- チャレンジ (難易度:⭐⭐⭐):MegaShopのすべてのコアモジュール + i18n + パフォーマンス最適化を完了し, Lighthouseスコア90+を達成してください
---|



