Next.js App Routerで個人ECサイトを作る全手順

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。
個人でECサイトを作るとき、「どの技術スタックを選ぶか」「決済はどう組み込むか」「どうやって本番で公開するか」という3つの壁が一気にのしかかります。この記事では、Next.js(App Router)+ TypeScript + Prisma + SQLite + PAY.JP という構成でECサイトを構築・運用した経験をもとに、環境構築から本番公開まで再現できる手順をまとめます。
技術スタックと全体構成
採用した構成は次のとおりです。
| レイヤー | 採用技術 | 採用理由 |
|---|---|---|
| フレームワーク | Next.js(App Router) | SSR・Route Handlerが一体化できる |
| 言語 | TypeScript | 型安全なままPrismaと連携できる |
| ORM | Prisma | スキーマ駆動でマイグレーションが自動化できる |
| DB | SQLite(prisma/dev.db) | 個人運営規模なら十分、単一ファイルでバックアップも簡単 |
| 決済 | PAY.JP | 日本円専用でREST APIが分かりやすい |
| 公開 | Cloudflare Tunnel | ポート開放なしで自宅サーバーからHTTPS公開できる |
「PostgreSQLにしないのか」とよく聞かれますが、個人運営のECサイトでは同時書き込みが集中することはほぼなく、SQLiteで十分動きます。バックアップも cp prisma/dev.db prisma/dev.db.bak の1コマンドで完結します。
商品画像は public/ ではなく別ディレクトリに保管し、Route Handler経由で配信する構成にしました。これにより、URLを知っていれば誰でも直接アクセスできる状態を避けられます。
プロジェクト作成とデータベース設計
npx create-next-app@latest ec-site --typescript --app --tailwind --eslint
cd ec-site
npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
prisma/schema.prisma に最小限のモデルを追加します。
model Product {
id Int @id @default(autoincrement())
name String
price Int
stock Int @default(0)
imageDir String
createdAt DateTime @default(now())
items OrderItem[]
}
model Order {
id Int @id @default(autoincrement())
email String
status String @default("pending")
createdAt DateTime @default(now())
items OrderItem[]
}
model OrderItem {
id Int @id @default(autoincrement())
order Order @relation(fields: [orderId], references: [id])
orderId Int
product Product @relation(fields: [productId], references: [id])
productId Int
quantity Int
}
スキーマを保存したらマイグレーションを実行します。
npx prisma migrate dev --name init
prisma/dev.db が生成されれば成功です。商品・注文テーブルができた状態から開発を始められます。
PAY.JPで決済フローを実装する
PAY.JPはクライアント側でトークンを生成し、サーバー側でそのトークンを使ってchargeを作成するという2ステップが基本です。EMV 3-Dセキュアが必要な場合は認証フローが追加されます。
charge作成とEMV 3-Dセキュア
// app/api/charge/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { token, amount, orderId } = await req.json();
const res = await fetch("https://api.pay.jp/v1/charges", {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(
process.env.PAYJP_SECRET_KEY + ":"
).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
card: token,
amount: String(amount),
currency: "jpy",
capture: "true",
three_d_secure: "true",
}),
});
const charge = await res.json();
// 3Dセキュア認証ページへのリダイレクトが必要な場合
if (charge.next_action?.type === "redirect_to_url") {
return NextResponse.json({
redirect_url: charge.next_action.redirect_to_url.url,
});
}
// 認証成功 → 在庫減算・注文確定
if (
charge.three_d_secure_status === "verified" ||
charge.three_d_secure_status === "attempted"
) {
await confirmOrder(orderId);
return NextResponse.json({ status: "ok", chargeId: charge.id });
}
// 決済失敗 → 在庫を戻す
await revertStock(orderId);
return NextResponse.json({ error: charge.error?.message }, { status: 400 });
}
3-Dセキュアのコールバックは PAY.JP ダッシュボードで設定したURLに飛んできます。コールバック先のRoute Handlerでchargeを取得し、three_d_secure_status が verified になっていることを確認してから注文を確定します。失敗時の在庫戻し忘れに注意してください。
環境変数は .env.local に記載し、リポジトリにコミットしないようにします。
PAYJP_SECRET_KEY=sk_live_xxxx
PAYJP_PUBLIC_KEY=pk_live_xxxx
テスト時は sk_test_xxxx / pk_test_xxxx を使います。PAY.JPのテスト環境ではカード番号 4242424242424242 で決済フローを通せます。
セキュリティ対策をひととおり入れる
決済が入るサイトでは最低限のセキュリティ対策が必要です。実際に導入したものを列挙します。
WAF層の実装
Next.jsのmiddlewareまたは独自プロキシモジュールで以下を実装しました。
- レート制限:300 req/分を超えたら429を返す
- ログイン失敗ロック:5回連続失敗で15分間ロック
- IP自動BAN:不審なリクエストパターンを検知して一時ブロック
- 画像アップロードのマジックバイト検証:
Content-Type: image/jpegでも先頭バイトがFF D8 FFでなければ拒否する
// マジックバイト検証(JPEG)
const buffer = Buffer.from(await file.arrayBuffer());
const isJpeg = buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
if (!isJpeg) {
return NextResponse.json({ error: "Invalid file type" }, { status: 400 });
}
ClamAVによるウイルススキャン
ファイルアップロードを受け取ったらサーバー側でスキャンをかけます。
sudo apt install clamav clamav-daemon
sudo systemctl enable clamav-freshclam
sudo systemctl start clamav-freshclam
# crontab -e で追加(毎朝4:30にスキャン)
30 4 * * * clamscan -r /home/user/ec-site/uploads --log=/var/log/clamscan.log
動作確認はEICARテストファイルで行います。EICARファイルを配置してスキャンを実行し、検知ログが出れば正常です。
npm auditの脆弱性対応
npm audit fix --force は依存関係を壊すことがあります。検出された脆弱性は package.json の overrides で特定バージョンに固定するほうが安全です。
{
"overrides": {
"vulnerable-package": "^2.1.0"
}
}
本番公開:systemd常駐とCloudflare Tunnel
自宅のLinuxサーバーで動かす場合、Cloudflare Tunnelを使えばポート開放なしでHTTPS公開できます。
1. アプリを本番ビルドする
npm run build
2. systemdサービスを作成する
# /etc/systemd/system/ec-site.service
[Unit]
Description=EC Site Next.js
After=network.target
[Service]
Type=simple
User=your-user
WorkingDirectory=/home/your-user/ec-site
ExecStart=/usr/bin/node node_modules/.bin/next start -p 3000
Restart=on-failure
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
node のパスは which node で確認してください。環境によって異なります。
sudo systemctl daemon-reload
sudo systemctl enable ec-site
sudo systemctl start ec-site
3. Cloudflare Tunnelで公開する
cloudflared tunnel create ec-site
cloudflared tunnel route dns ec-site yourdomain.com
設定ファイル(~/.cloudflared/config.yml)を作成します。
tunnel: <TUNNEL_ID>
credentials-file: /home/your-user/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: yourdomain.com
service: http://localhost:3000
- service: http_status:404
cloudflared tunnel run ec-site
cloudflared 自体もsystemdで常駐させておくと、サーバー再起動後も自動で復帰します。
つまずきやすい点:dev serverのonClickがLAN内で動かない
next dev で起動したサーバーにLAN内の別端末(スマホなど)からアクセスすると、ページは表示されるのにonClickが一切発火しない現象がありました。next build && next start のプロダクションビルドに切り替えたところ、同じ経路で正常に動作しました。開発中に実機確認が必要な場合はプロダクションビルドで試すようにしてください。環境によって再現するかどうかは異なる可能性があります。
まとめ
Next.js(App Router)+ Prisma + SQLite + PAY.JPという構成で、個人運営のECサイトは十分に本番運用できます。特にはまりやすいのは3-Dセキュアのコールバック処理と失敗時の在庫戻し、それとdev serverとプロダクションビルドの挙動差の2点です。
まず npx create-next-app でプロジェクトを作り、Prismaのマイグレーションを通してからPAY.JPのテスト環境で決済フローを一通り確認するのが最短の進め方です。サイトが動いた後に何が必要かについては、個人開発で収益化する現実:動くものを作った後に何が必要かも参考にしてください。
よくある質問
Next.jsで個人ECサイトを作るとき、データベースは何を使えばいいですか?
個人運営規模であればSQLiteで十分です。Prismaと組み合わせることでスキーマ管理やマイグレーションが自動化でき、バックアップも単一ファイルをコピーするだけで完結します。
PAY.JPのEMV 3-Dセキュアはどう実装しますか?
charge作成時にthree_d_secureをtrueにし、レスポンスのnext_actionにリダイレクトURLが含まれる場合は認証ページへ誘導します。コールバックでthree_d_secure_statusがverifiedになっていることを確認してから注文を確定します。
自宅サーバーでNext.jsをHTTPS公開するにはどうすればいいですか?
Cloudflare Tunnelを使えばポート開放なしでHTTPS公開できます。cloudflaredでトンネルを作成し、設定ファイルでローカルポートとドメインを紐付けるだけで動作します。
Next.jsのdev serverでスマホからアクセスするとonClickが動かない場合は?
next build && next startでプロダクションビルドを起動すると解消します。dev serverとプロダクションビルドの挙動差によるものと考えられますが、環境によって再現するかどうかは異なる可能性があります。