【Next.js,Stripe】Next.js14のAPI Routes & Stripeを用いた課金機能を実装する
はじめに
今回は、Next.jsでStripeを用いた課金機能の実装を、備忘録として簡単に書いていきます。
Webサイトを作る人は、これで収益化できます。
Next.jsのバージョンは14.1.0です。
Stripeのアカウントを作成する方は以下の記事を参考に用意してください。
準備
Next.jsのプロジェクト作成
作成
任意のディレクトリで、以下のコマンドを用いて作成します。
npx create next-app@latest [プロジェクト名]
選択肢は全てデフォルトにしておきます。以下の画像のようになればok。
globals.cssの編集
app/globals.css
のファイルの中身を以下のように編集します。
@tailwind base;
@tailwind components;
@tailwind utilities;
page.tsxの編集
app/page.tsx
のファイルの中身を以下のように編集しておきます。
export default function Home() {
return (
<div>
<h1>Home</h1>
</div>
);
}
.envファイルを作成
環境変数を設定します。ルートディレクトリに.env
ファイルを作成し、以下のように編集します。
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET_KEY=
NEXT_PUBLIC_PRICE_ID_MONTH=
NEXT_PUBLIC_PRICE_ID_HALF_YEAR=
NEXT_PUBLIC_PRICE_ID_YEAR=
URL=ngrokなどを発行してください
URL
以外、全てStripeのダッシュボードにて取得したものを入れます。今回は、3種類のサブスクリプションプランを用意しています。
また、Stripeのダッシュボードにて、発行したngrok/api/webhook
の形式のwebhookを発行してください。
サーバーサイド
app/api/create-checkout-session/route.tsを作成
Stripeのチェックアウトセッションを作成するためのAPIエンドポイントを作成します。
app
ディレクトリの配下にcreate-checkout-session
フォルダを作成し、その直下にroute.ts
ファイルを作成します。
import Stripe from "stripe";
import { NextResponse } from "next/server";
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
export async function POST(req: Request) {
try {
const body = await req.json();
if (!body.priceId) {
throw new Error("Missing price_id");
}
const priceId = body.priceId as string;
const userId = body.userId as string;
let customerId = ''
const params: Stripe.Checkout.SessionCreateParams = {
customer: customerId,
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: "subscription",
success_url: `https://${process.env.URL}/success`,
cancel_url: `https://${process.env.URL}/`,
metadata: { userID: userId },
};
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create(params);
return NextResponse.json({ result: checkoutSession, ok: true });
} catch (error) {
console.error(error);
return NextResponse.json(
{ message: "something went wrong", ok: false },
{ status: 500 }
);
}
}
ユーザーを用意したデータベースで管理するための一例として、チェックアウトセッションを作成するパラメータの部分で、
metadata: { userID: userId }
として、クライアントから受け取ったuserId
を持たせています。
また、パラメータのうち以下の部分は、決済が成功した後またはキャンセルした後に遷移するurlを指定しています。
success_url: `https://${process.env.URL}/success`,
cancel_url: `https://${process.env.URL}/`,
app/api/webhook/route.tsを作成
StripeのWebhookを処理するためのファイルを作成します。
決済が完了した際に、Stripeからレスポンスがリアルタイムにきます。そのレスポンスを受け取り、データベースの更新やユーザーに対してメールを送信するエンドポイントを作成します。
import { NextRequest, NextResponse } from 'next/server';
import { headers } from "next/headers";
import Stripe from 'stripe';
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.NEXT_PUBLIC_STRIPE_WEBHOOK_SECRET_KEY;
export async function POST(req: NextRequest, res: NextResponse) {
try {
const signature = headers().get("stripe-signature");
if (!signature) {
return NextResponse.json({
message: 'Bad request. No Stripe signature'
}, {
status: 400
})
}
const body = await req.text();
const event = stripe.webhooks.constructEvent(
body,
signature,
endpointSecret
);
if (event.type === 'checkout.session.completed') {
const checkoutSessionEvent = event.data.object as Stripe.Checkout.Session
if (!checkoutSessionEvent.customer_details?.email) {
throw new Error(`missing user email, ${event.id}`);
}
const userId = checkoutSessionEvent?.metadata?.userID as string
const stripeId = (event.data.object as Stripe.Checkout.Session)?.customer as string
const email = (event.data.object as Stripe.Checkout.Session)?.customer_details?.email as string
updateDatabase(userId);
sendEmail(email);
}
return NextResponse.json({
message: 'Success'
}, {
status: 200
})
} catch (err) {
console.error("Error processing webhook", {
error: (err as Error).message,
stack: (err as Error).stack,
method: req.method,
url: req.url,
body: req.body
});
return new Response("Internal Server Error", { status: 500 });
}
}
以下の部分で、決済が完了した際にStripeから送られてくるイベントを元にデータベースの更新やメールの送信などの処理をさせています。
if (event.type === 'checkout.session.completed') {
const checkoutSessionEvent = event.data.object as Stripe.Checkout.Session
if (!checkoutSessionEvent.customer_details?.email) {
throw new Error(`missing user email, ${event.id}`);
}
const userId = checkoutSessionEvent?.metadata?.userID as string
const stripeId = (event.data.object as Stripe.Checkout.Session)?.customer as string
const email = (event.data.object as Stripe.Checkout.Session)?.customer_details?.email as string
updateDatabase(userId);
sendEmail(email);
}
クライアントサイド
app/page.tsxの編集
"use client"
import React, { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string);
export default function Home() {
const [userId, setUserId] = useState('');
const handlePayment = async (priceId: string) => {
if (!userId) {
console.log("User ID is not available.");
return <div>Loading...</div>;
}
if (!priceId) {
console.log("Price ID is not available.");
return <div>Loading...</div>;
}
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: userId,
priceId: priceId,
}),
});
const json = await response.json();
if (!json.ok) {
throw new Error("Failed to create checkout session.");
}
const stripe = await stripePromise;
if (!stripe) {
throw new Error("Stripe.js has not loaded.");
}
await stripe.redirectToCheckout({ sessionId: json.result.id });
};
return (
<section style={{ display: 'flex', justifyContent: 'space-around' }}>
<div className="product">
<div className="description">
<h3>1ヶ月プラン</h3>
<h5>$1</h5>
</div>
<button onClick={() => handlePayment(process.env.NEXT_PUBLIC_PRICE_ID_MONTH as string)}>Checkout</button>
</div>
<div className="product">
<div className="description">
<h3>6ヶ月プラン</h3>
<h5>$6</h5>
</div>
<button onClick={() => handlePayment(process.env.NEXT_PUBLIC_PRICE_ID_HALF_YEAR as string)}>Checkout</button>
</div>
<div className="product">
<div className="description">
<h3>年間プラン</h3>
<h5>$12</h5>
</div>
<button onClick={() => handlePayment(process.env.NEXT_PUBLIC_PRICE_ID_YEAR as string)}>Checkout</button>
</div>
</section>
);
}
クライアントサイドなので、冒頭に"use client"
を忘れずにつけ、クライアントコンポーネントにします。
npm run dev
をすると、以下のような画面になります。
checkout
ボタンを押すと、各ボタンに対応するpriceId
とuserId
がJSON形式で、サーバーサイドで作成したStripeのチェックアウトセッションを作成するためのAPIエンドポイントに飛ばされます。
Discussion