YassineBouchama
nextjs

Complete Guide: Setting Up Stripe with Next.js 15 App Router and TypeScript

Complete Guide: Setting Up Stripe with Next.js 15 App Router and TypeScript
0 views
5 min read
#nextjs

Setting Up Stripe with Next.js 15 App Router and TypeScript

In this comprehensive guide, we'll walk through the process of integrating Stripe payments into a Next.js 15 application using the App Router and TypeScript.

Prerequisites

Before we begin, make sure you have:

  1. Node.js installed (version 18.17 or later)
  2. A Stripe account (sign up at stripe.com)
  3. Next.js 15 project with TypeScript
  4. Stripe CLI installed (optional, for webhook testing)

Installation

First, install the required dependencies:

npm install stripe @stripe/stripe-js

Environment Setup

Create or update your .env.local file:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret

Type Definitions

Create a types file types/stripe.ts:

export interface Price {
  id: string;
  product_id: string;
  active: boolean;
  currency: string;
  unit_amount: number;
}

export interface Product {
  id: string;
  name: string;
  description: string;
  active: boolean;
  prices?: Price[];
}

Stripe Configuration

Create a utility file lib/stripe.ts:

import Stripe from 'stripe';

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error('Missing Stripe secret key');
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2023-10-16', // Use the latest API version
  typescript: true,
});

Server Actions Setup

Create a server action file app/actions/stripe.ts:

'use server'

import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';

export async function createCheckoutSession(price_id: string) {
  const headersList = headers();
  const origin = headersList.get('origin');

  try {
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items: [
        {
          price: price_id,
          quantity: 1,
        },
      ],
      success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${origin}/canceled`,
    });

    return { sessionId: session.id };
  } catch (error) {
    console.error('Error creating checkout session:', error);
    throw error;
  }
}

Client-Side Implementation

Create a checkout button component components/CheckoutButton.tsx:

'use client';

import { loadStripe } from '@stripe/stripe-js';
import { useState } from 'react';
import { createCheckoutSession } from '@/app/actions/stripe';

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

interface CheckoutButtonProps {
  priceId: string;
}

export default function CheckoutButton({ priceId }: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    try {
      setLoading(true);
      const { sessionId } = await createCheckoutSession(priceId);
      const stripe = await stripePromise;
      
      if (!stripe) throw new Error('Stripe failed to initialize');

      const { error } = await stripe.redirectToCheckout({
        sessionId,
      });

      if (error) {
        throw error;
      }
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handleCheckout}
      disabled={loading}
      className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
    >
      {loading ? 'Loading...' : 'Buy Now'}
    </button>
  );
}

Webhook Handler

Create a webhook route handler app/api/webhook/route.ts:

import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature')!;

  try {
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case 'checkout.session.completed':
        const session = event.data.object;
        // Handle successful payment
        console.log('Payment successful:', session);
        break;
      // Add other webhook events as needed
    }

    return new Response('Webhook received', { status: 200 });
  } catch (error) {
    console.error('Webhook error:', error);
    return new Response(
      'Webhook error: ' + (error as Error).message,
      { status: 400 }
    );
  }
}

export const runtime = 'edge';

Usage Example

Here's how to implement a product page with Stripe checkout:

// app/products/[id]/page.tsx
import { stripe } from '@/lib/stripe';
import CheckoutButton from '@/components/CheckoutButton';

interface ProductPageProps {
  params: {
    id: string;
  };
}

export default async function ProductPage({ params }: ProductPageProps) {
  const product = await stripe.products.retrieve(params.id, {
    expand: ['default_price'],
  });

  const price = product.default_price as Stripe.Price;

  return (
    <div className="p-4">
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>
        Price: {new Intl.NumberFormat('en-US', {
          style: 'currency',
          currency: price.currency,
        }).format((price.unit_amount || 0) / 100)}
      </p>
      <CheckoutButton priceId={price.id} />
    </div>
  );
}

Testing Webhooks Locally

To test webhooks during development:

  1. Install the Stripe CLI
  2. Run the following command:
stripe listen --forward-to localhost:3000/api/webhook

Best Practices and Security Considerations

  1. Environment Variables

    • Never expose your Stripe secret key
    • Use different keys for development and production
  2. Error Handling

    • Implement proper error handling for all Stripe operations
    • Log errors appropriately
  3. TypeScript Types

    • Use Stripe's built-in TypeScript types
    • Create custom types for your specific implementation
  4. Webhook Security

    • Always verify webhook signatures
    • Use environment variables for webhook secrets
  5. Payment Flow

    • Implement proper success and error pages
    • Handle edge cases and failed payments

Conclusion

You now have a fully functional Stripe integration in your Next.js 15 application with TypeScript support. This setup provides:

  • Type-safe Stripe operations
  • Secure payment processing
  • Webhook handling
  • Server-side and client-side integration

Remember to:

  • Test thoroughly in development
  • Use Stripe's test mode before going live
  • Monitor your Stripe dashboard for transactions
  • Keep your dependencies updated

For more advanced features like subscriptions, recurring payments, or customer management, refer to the Stripe documentation.


This implementation provides a solid foundation for handling payments in your Next.js application. You can build upon this setup to add more complex payment flows and features as needed.