DEV Community

GLD5000
GLD5000

Posted on

Add stripe to a Next 13.4 App

This is a guide to make a Next.js project with Stripe Payment GateWay

TLDR: grab the template here

Also including:

  1. TypeScript for type safety
  2. Tailwind CSS for styling
  3. Jest for testing
  4. ESLint for linting
  5. Prettier for formatting
  6. Husky & Lint-Staged for pre-commit testing and linting

If you like this, checkout my other projects on GitHub or via my Portfolio

Setup Stripe with Next 13.4

To set up or better understand the project you can read on!

Installation

  1. Start with a clean Next.js install or my GLD-NextTemplate
  2. If starting with the GLD-NextTemplate, run npm i to set up dependencies etc.
  3. Run npm i stripe axios to install stripe and axios (You can also just use the native Fetch API if you like).

Setup Account on Stripe

  1. Go to Stripe and setup an account with your email address (You do not need a credit card or anything as you can just stay in 'test mode').
  2. Copy your secret key from the 'Developers' Tab and put it into a '.env' file in your repos root directory.
  3. Add some products using the 'Products' tab (we will use the API to fetch these for our page).

Create API route handlers

You will need two handlers:

  1. To fetch products from your Stripe account
  2. To enable checkout for a desired product

api/getproducts/route.ts:

import Stripe from 'stripe';
import { NextResponse } from 'next/server';

/* eslint-disable import/prefer-default-export */

export async function GET() {
    if (process.env.STRIPE_SECRET_KEY) {
        const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
            apiVersion: '2022-11-15',
        });
        const products = await stripe.products.list({
            limit: 4,
            expand: ['data.default_price'],
        });
        // console.log('products:', products)
        return NextResponse.json(products.data);
    }
}

Enter fullscreen mode Exit fullscreen mode

api/checkout/route.ts:

import Stripe from 'stripe';
import { NextResponse, NextRequest } from 'next/server';

/* eslint-disable import/prefer-default-export */

export async function POST(request: NextRequest) {
    if (process.env.STRIPE_SECRET_KEY) {
        const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
            apiVersion: '2022-11-15',
        });
        const data = await request.json();
        const { priceId } = data;
        const session = await stripe.checkout.sessions.create({
            line_items: [
                {
                    price: priceId,
                    quantity: 1,
                },
            ],
            mode: 'payment',
            success_url: `${process.env.HOST_URL}`,
            cancel_url: `${process.env.HOST_URL}`,
        });

        return NextResponse.json(session.url);
    }
}

Enter fullscreen mode Exit fullscreen mode

Integrate getproducts (AKA fetch) API route

The fetch is be enabled by a useEffect on page / site load:

    useEffect(() => {
        fetchProducts();
    }, []);
Enter fullscreen mode Exit fullscreen mode

The fetchProducts function gets an array of products and sets the component state to the result:

    async function fetchProducts() {
        const { data } = await axios.get('/api/getproducts');
        setProducts(data);
    }

Enter fullscreen mode Exit fullscreen mode

We map over the returned array, sending each product to a product card:

    function getProductCards(productData: Product[]) {
        return productData.map((data) => (
            <ProductCard key={data.id} data={data} />
        ));
    }
Enter fullscreen mode Exit fullscreen mode

Then we return the product card array to the page:

   return (
        <div className="grid sm:grid-cols-2 gap-16">
            {getProductCards(products)}
        </div>
    );
Enter fullscreen mode Exit fullscreen mode

I put these parts together in a 'ProductCards.tsx':

'use client';

import axios from 'axios';
import { useState, useEffect } from 'react';
import { Product } from '@/lib/stripe/types';
import ProductCard from './ProductCard';

export default function ProductCards() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        fetchProducts();
    }, []);

    return (
        <div className="grid sm:grid-cols-2 gap-16">
            {getProductCards(products)}
        </div>
    );

    async function fetchProducts() {
        const { data } = await axios.get('/api/getproducts');
        setProducts(data);
    }

    function getProductCards(productData: Product[]) {
        return productData.map((data) => (
            <ProductCard key={data.id} data={data} />
        ));
    }
}


Enter fullscreen mode Exit fullscreen mode

Integrate the checkout API Route

In the product card, there is a button for buying a product:

  <Button onClick={handleClickBuy}>Buy Now</Button>

Enter fullscreen mode Exit fullscreen mode

This button has a click handler which makes the API request and gives control of the window location to the Stripe checkout returned:

    async function handleClickBuy(e: SyntheticEvent) {
        e.preventDefault();
        const { data: checkout } = await axios.post(
            '/api/checkout',
            {
                priceId: id,
            },
            {
                headers: {
                    'Content-Type': 'application/json',
                },
            }
        );
        window.location.assign(checkout);
    }

Enter fullscreen mode Exit fullscreen mode

I put these in a ProductCard.tsx component:

import Image from 'next/image';
import axios from 'axios';
import { SyntheticEvent } from 'react';
import { Price, Product } from '@/lib/stripe/types';
import { Button } from '../ui/button';

export default function ProductCard({ data }: { data: Product }) {
    const {
        name,
        description,
        images: [image],
        default_price: price,
    } = data;
    const { currency, unit_amount: amount, id } = price as Price;
    if (!name || !description || !image) return null;
    return (
        <div className="grid gap-4 border rounded shadow p-6 lg:max-w-[35rem]">
            <Image
                priority
                src={image}
                alt={description}
                width={806}
                height={671}
            />
            <h2 className="text-6xl font-bold mx-auto w-fit">{name}</h2>
            <p className="mx-auto">{description}</p>
            <b className="mx-auto">{`${
                amount ? amount * 0.01 : 'N/A'
            } ${currency}`}</b>
            <Button onClick={handleClickBuy}>Buy Now</Button>
        </div>
    );

    async function handleClickBuy(e: SyntheticEvent) {
        e.preventDefault();
        const { data: checkout } = await axios.post(
            '/api/checkout',
            {
                priceId: id,
            },
            {
                headers: {
                    'Content-Type': 'application/json',
                },
            }
        );
        window.location.assign(checkout);
    }
}

Enter fullscreen mode Exit fullscreen mode

On Types

I also decided to destructure the type definitions for 'Product' and 'Price' and export them from my own module so I did not need to keep importing the stripe module and using dot notation to access them (lib/stripe/types.ts):

import Stripe from 'stripe';

export type Product = Stripe.Product;
export type Price = Stripe.Price;
Enter fullscreen mode Exit fullscreen mode

That's about it- remember to like / star if you found this useful!

Top comments (0)