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

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とプロダクションビルドの挙動差によるものと考えられますが、環境によって再現するかどうかは異なる可能性があります。