個人開発SaaSにStripeサブスク課金をTypeScriptで実装する手順
結論:Stripe Checkoutで最速サブスク課金を実装する
個人開発SaaSにサブスク課金を組み込む最短ルートは、Stripe Checkoutのホスティングページを使う構成です。自前でカード入力フォームを作らず、StripeのCheckoutセッションURLにリダイレクトするだけで、PCI DSS準拠・3Dセキュア対応・モバイル最適化済みの決済画面が手に入ります。
この記事では TypeScript + Hono(またはNext.js APIルート)でバックエンドを実装し、Webhookのローカルテストから本番デプロイまでを一気通貫で解説します。
全体アーキテクチャ
ブラウザ
│ ①「プランを購入」ボタン押下
▼
バックエンド(Hono / Next.js)
│ ②Checkoutセッション作成 → セッションURL返却
▼
Stripe Checkout(ホスティング決済画面)
│ ③支払い完了
▼
Webhook(/api/stripe/webhook)
│ ④checkout.session.completed を受信
▼
DB(サブスク状態を更新)
DBはPlanetScale / Supabase など何でも構いません。本記事ではDB操作部分は疑似コードで表します。
1. Stripe・依存パッケージのセットアップ
# Stripeパッケージ
npm install stripe @stripe/stripe-js
# ローカルWebhookテスト用CLI
# https://stripe.com/docs/stripe-cli
brew install stripe/stripe-cli/stripe
.env.local に環境変数を追加します。
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000
注意:
sk_test_はテストキーです。本番はsk_live_を使い、環境変数を分けて管理してください。
2. Stripeクライアントのシングルトン初期化
// lib/stripe.ts
import Stripe from "stripe";
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is not set");
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-04-10", // 最新APIバージョンを指定
typescript: true,
});
3. Checkoutセッション作成エンドポイント
Honoの場合
// routes/checkout.ts
import { Hono } from "hono";
import { stripe } from "../lib/stripe";
const app = new Hono();
app.post("/api/stripe/checkout", async (c) => {
const { priceId, userId } = await c.req.json<{
priceId: string;
userId: string;
}>();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
// Webhookでユーザーを特定するためのメタデータ
metadata: { userId },
// 既存顧客IDがあればここで渡す(サブスク管理が容易になる)
// customer: existingStripeCustomerId,
});
return c.json({ url: session.url });
});
export default app;
Next.js App Routerの場合
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const { priceId, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
metadata: { userId },
});
return NextResponse.json({ url: session.url });
}
フロントエンド(共通)
// components/CheckoutButton.tsx
"use client";
const handleCheckout = async (priceId: string) => {
const res = await fetch("/api/stripe/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId, userId: "user_123" }),
});
const { url } = await res.json();
window.location.href = url; // Stripeのホスティングページへリダイレクト
};
4. Webhookの実装(最重要)
Checkoutが完了したあと、Stripeはあなたのサーバーに checkout.session.completed を送信します。この受信処理がサブスク管理の核です。
// app/api/stripe/webhook/route.ts(Next.js例)
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const body = await req.text(); // rawBodyが必要
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error("Webhook signature verification failed:", err);
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const userId = session.metadata?.userId;
const subscriptionId = session.subscription as string;
// DBにサブスク情報を保存する
// await db.user.update({ where: { id: userId }, data: { subscriptionId, plan: "pro" } });
console.log(`User ${userId} subscribed: ${subscriptionId}`);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
// サブスクキャンセル時の処理
// await db.user.update({ where: { stripeSubscriptionId: subscription.id }, data: { plan: "free" } });
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}
重要: Next.js App RouterではWebhookルートに
bodyParserが自動で適用されます。req.text()でrawボディを取得することで署名検証が正しく動作します。
5. Webhookのローカルテスト(Stripe CLI)
Stripeダッシュボードを確認しながら、ローカル環境でWebhookをリアルタイムにテストできます。
# StripeのWebhookをローカルにフォワード
stripe listen --forward-to localhost:3000/api/stripe/webhook
# 別ターミナルでイベントを手動送信
stripe trigger checkout.session.completed
stripe listen を起動すると whsec_xxxx 形式のWebhookシークレットが表示されます。これを .env.local の STRIPE_WEBHOOK_SECRET に設定します。
6. Stripe Dashboardで商品・価格を作成する
| 設定項目 | 値の例 |
|---|---|
| 商品名 | Pro Plan |
| 価格 | ¥980 / 月 |
| 課金タイプ | 繰り返し(recurring) |
| 価格ID | price_xxxxxxxxxx |
ダッシュボード → 製品カタログ → 「商品を追加」で作成し、Price ID(price_ から始まる文字列)をフロントのボタンに渡します。
7. 本番デプロイ時のチェックリスト
- 環境変数を
sk_live_/pk_live_に切り替える - Stripeダッシュボードで本番用Webhookエンドポイントを登録(
https://yourdomain.com/api/stripe/webhook) - Webhookシークレット(
whsec_)を本番の環境変数に設定 - 成功URL・キャンセルURLを本番ドメインに更新
- Stripeのテストカード(
4242 4242 4242 4242)で動作確認後、本番モード有効化申請
よくある落とし穴と対処法
❌ Webhookの署名検証エラー
bodyParser や express.json() が先にボディをパースしてしまうと署名検証が失敗します。StripeのWebhookルートには必ず rawボディ を渡すか、該当ルートだけ bodyParser を無効にしてください。
❌ Webhookの二重処理
Stripeは同じイベントを複数回送信することがあります(リトライ)。event.id をDBに記録してべき等性を担保しましょう。
// 処理済みイベントIDをチェック
const processed = await db.stripeEvent.findUnique({ where: { id: event.id } });
if (processed) return NextResponse.json({ received: true }); // スキップ
await db.stripeEvent.create({ data: { id: event.id } });
❌ サブスク更新・失敗の未対応
invoice.payment_failed や customer.subscription.updated も必ずハンドリングしてください。特に支払い失敗時にプランをダウングレードしないと、無料利用が続いてしまいます。
まとめ
| ステップ | やること |
|---|---|
| 1 | Stripe CLIとパッケージをインストール |
| 2 | stripe.checkout.sessions.create でセッション作成・リダイレクト |
| 3 | /api/stripe/webhook でイベント受信・DB更新 |
| 4 | stripe listen でローカルWebhookテスト |
| 5 | 本番キーに切り替えてデプロイ |
Stripe CheckoutとWebhookの組み合わせは、個人開発SaaSのマネタイズとして最もコスパが高い構成です。カード情報を自前で扱わないため、セキュリティ面のリスクも最小化できます。まずはテストモードで動かしてみてください。
SaaSを公開するサーバーには、高速でコスパの良いVPSを選ぶと開発体験が向上します。{{A8:xserverVps}}