<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ stripe - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ stripe - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:48:15 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/stripe/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Online Marketplace with Next.js, Express, and Stripe Connect ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever wondered how platforms like Etsy, Uber, or Teachable handle payments for thousands of sellers? The answer is a multi-vendor marketplace: an application where merchants can sign up, list  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-online-marketplace-with-next-js-express-stripe-connect/</link>
                <guid isPermaLink="false">69d7ca9dfa7251682ec4b098</guid>
                
                    <category>
                        <![CDATA[ stripe ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Michael Okolo ]]>
                </dc:creator>
                <pubDate>Thu, 09 Apr 2026 15:49:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1181805a-87ae-440d-9673-64efeb073aad.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever wondered how platforms like Etsy, Uber, or Teachable handle payments for thousands of sellers? The answer is a <strong>multi-vendor marketplace</strong>: an application where merchants can sign up, list products or services, and receive payments directly from customers.</p>
<p>In this handbook, you'll build a complete marketplace from scratch using TypeScript. You won't need a traditional database. Instead, you'll use Stripe as your product catalog and payment engine.</p>
<p>This is how many real-world marketplaces work: Stripe stores the products, prices, and customer data, while your application handles the user experience.</p>
<p>Here's what you'll build:</p>
<ol>
<li><p>A merchant onboarding flow where sellers create accounts and connect with Stripe</p>
</li>
<li><p>A product management system where merchants can add and list products directly through Stripe</p>
</li>
<li><p>A checkout flow that supports both one-time payments and recurring subscriptions</p>
</li>
<li><p>Webhooks that listen for payment events in real time</p>
</li>
<li><p>A billing portal where customers can manage their subscriptions</p>
</li>
<li><p>A complete storefront where customers can browse and buy products</p>
</li>
</ol>
<p>You can also grab the complete source code from the GitHub repository linked at the end.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-stripe-connect">What is Stripe Connect?</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-project">How to Set Up the Project</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-backend">How to Set Up the Backend</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-express-backend">How to Build the Express Backend</a></p>
</li>
<li><p><a href="#heading-how-to-handle-merchant-onboarding">How to Handle Merchant Onboarding</a></p>
<ul>
<li><p><a href="#heading-how-to-create-a-connected-account">How to Create a Connected Account</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-onboarding-link">How to Create the Onboarding Link</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-check-account-status">How to Check Account Status</a></p>
</li>
<li><p><a href="#heading-how-to-create-products-through-stripe">How to Create Products Through Stripe</a></p>
</li>
<li><p><a href="#heading-how-to-fetch-products">How to Fetch Products</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-checkout-flow">How to Build the Checkout Flow</a></p>
</li>
<li><p><a href="#heading-how-to-handle-webhooks">How to Handle Webhooks</a></p>
</li>
<li><p><a href="#heading-how-to-configure-webhooks-in-the-stripe-dashboard">How to Configure Webhooks in the Stripe Dashboard</a></p>
</li>
<li><p><a href="#heading-how-to-test-webhooks-locally">How to Test Webhooks Locally</a></p>
</li>
<li><p><a href="#heading-how-to-add-the-billing-portal">How to Add the Billing Portal</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-nextjs-frontend">How to Build the Next.js Frontend</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-account-context">How to Create the Account Context</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-account-status-hook">How to Create the Account Status Hook</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-merchant-onboarding-component">How to Build the Merchant Onboarding Component</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-product-create-product-list-and-checkout">How to Build the Product Create, Product List and Checkout</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-product-form">How to Build the Product Form</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-main-page">How to Build the Main Page</a></p>
</li>
<li><p><a href="#heading-how-to-test-the-full-flow">How to Test the Full Flow</a></p>
</li>
<li><p><a href="#heading-how-the-payment-split-works">How the Payment Split Works</a></p>
</li>
<li><p><a href="#heading-next-steps">Next Steps</a></p>
</li>
<li><p><a href="#heading-acknowledgements">Acknowledgements</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you begin, make sure you have the following:</p>
<ol>
<li><p>Node.js (version 18 or higher) installed on your machine</p>
</li>
<li><p>A basic understanding of React, TypeScript, and REST APIs</p>
</li>
<li><p>A Stripe account (sign up for free at <a href="http://stripe.com">stripe.com</a>)</p>
</li>
<li><p>A code editor like VS Code</p>
</li>
</ol>
<p>You do <strong>not</strong> need a database for this project. Stripe will store your products, prices, and customer information. This keeps the architecture simple and mirrors how many production marketplaces actually work.</p>
<h2 id="heading-what-is-stripe-connect"><strong>What is Stripe Connect?</strong></h2>
<p>Stripe Connect is a set of APIs designed for platforms and marketplaces. It lets you create accounts for your merchants (Stripe calls them "connected accounts"), route payments to them, and take a platform fee on every transaction.</p>
<p>In this tutorial, you will use Stripe’s <strong>V2 Accounts API</strong>, which is the newer and recommended way to create connected accounts. With the V2 API, you configure what each account can do (accept card payments, receive payouts) through a configuration object, and Stripe handles all compliance and identity verification through a hosted onboarding flow.</p>
<p>Here's how the payment flow works:</p>
<ol>
<li><p>A customer selects a product and clicks checkout on your marketplace.</p>
</li>
<li><p>Your server creates a Stripe Checkout Session linked to the merchant’s connected account.</p>
</li>
<li><p>The customer pays on Stripe’s hosted checkout page.</p>
</li>
<li><p>Stripe automatically splits the payment: the merchant gets their share, and your platform keeps an application fee.</p>
</li>
<li><p>Stripe sends a webhook event to your server confirming the payment.</p>
</li>
<li><p>The merchant can view their earnings and withdraw funds from their Stripe dashboard.</p>
</li>
</ol>
<h2 id="heading-how-to-set-up-the-project"><strong>How to Set Up the Project</strong></h2>
<p>Create a project folder with separate directories for your backend and frontend:</p>
<pre><code class="language-shell">mkdir marketplace &amp;&amp; cd marketplace
mkdir server client
</code></pre>
<h2 id="heading-how-to-set-up-the-backend"><strong>How to Set Up the Backend</strong></h2>
<p>Navigate into the server directory and initialize a TypeScript project:</p>
<pre><code class="language-shell">cd server
npm init -y
npm install express cors dotenv stripe
npm install -D typescript ts-node @types/express @types/cors @types/node
npx tsc --init
mkdir src
</code></pre>
<p>Open tsconfig.json and update it with these settings:</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}
</code></pre>
<p>Then create a .env file in the server root:</p>
<pre><code class="language-plaintext">STRIPE_SECRET_KEY=sk_test_your_key_here
DOMAIN=http://localhost:3000
</code></pre>
<p>You can find your Stripe test secret key in the Stripe Dashboard under Developers &gt; API Keys. The DOMAIN variable tells your server where to redirect customers after checkout.</p>
<p>Add these scripts to your package.json:</p>
<pre><code class="language-json">{
&nbsp; "scripts": {
    "dev": "ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
</code></pre>
<h2 id="heading-how-to-build-the-express-backend"><strong>How to Build the Express Backend</strong></h2>
<p>Create the file src/index.ts. This will be your entire backend. Let’s start with the setup and imports:</p>
<pre><code class="language-typescript">import express, { Request, Response, Router } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import Stripe from 'stripe';

dotenv.config();

const app = express();
const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);

app.use(cors({ origin: process.env.DOMAIN }));
app.use(express.static('public'));
</code></pre>
<p>Notice that we don't import any database client. Stripe is our data layer. Every product, price, customer, and transaction lives in Stripe. Your Express server is a thin orchestration layer that talks to the Stripe API on behalf of your frontend.</p>
<p>We also mount <code>express.static("public")</code> so you can serve static files later if needed. The webhook endpoint needs the raw request body, so we'll register it before the JSON parser. Let’s add that now.</p>
<h2 id="heading-how-to-handle-merchant-onboarding"><strong>How to Handle Merchant Onboarding</strong></h2>
<p>The first thing a merchant needs to do is create an account on your platform and connect it to Stripe. This involves two steps: creating a connected account, and then redirecting the merchant to Stripe’s hosted onboarding form.</p>
<h3 id="heading-how-to-create-a-connected-account">How to Create a Connected Account</h3>
<p>Add the following route to your src/index.ts:</p>
<pre><code class="language-typescript">// Type definitions for request bodies
interface CreateAccountBody {
  email: string;
}
interface AccountIdBody {
  accountId: string;
}

// Create a Connected Account using Stripe V2 API
router.post(
  '/create-connect-account',
  async (req: Request&lt;{}, {}, CreateAccountBody&gt;, res: Response) =&gt; {
    try {
      const account = await stripe.v2.core.accounts.create({
        display_name: req.body.email,
        contact_email: req.body.email,
        dashboard: 'full',
        defaults: {
          responsibilities: {
            fees_collector: 'stripe',
            losses_collector: 'stripe',
          },
        },
        identity: {
          country: 'GB',
          entity_type: 'company',
        },
        configuration: {
          customer: {},
          merchant: {
            capabilities: {
              card_payments: { requested: true },
            },
          },
        },
      });
      res.json({ accountId: account.id });
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      res.status(500).json({ error: message });
    }
  },
);
</code></pre>
<p>Let’s break down what this code does. The <code>stripe.v2.core.accounts.create()</code> method creates a new connected account using Stripe’s V2 API. Here are the key configuration options:</p>
<ol>
<li><p><code>dashboard: "full"</code> gives the merchant access to their own Stripe dashboard where they can view payments, manage payouts, and handle disputes.</p>
</li>
<li><p><code>responsibilities</code> tells Stripe who collects fees and who is liable for losses. Setting both to "stripe" means Stripe handles this, which is the simplest configuration.</p>
</li>
<li><p><code>identity</code> sets the country and entity type. Change "GB" to your merchants’ country code (for example, "US" for the United States).</p>
</li>
<li><p><code>configuration.merchant.capabilities</code> requests the <code>card_payments</code> capability, which lets the merchant accept credit card payments.</p>
</li>
</ol>
<h3 id="heading-how-to-create-the-onboarding-link">How to Create the Onboarding Link</h3>
<p>After creating the account, you need to redirect the merchant to Stripe’s hosted onboarding form. Add this route:</p>
<pre><code class="language-typescript">// Create Account Link for onboarding
router.post('/create-account-link', async (req: Request&lt;{}, {}, AccountIdBody&gt;, res: Response) =&gt; {
  const { accountId } = req.body;
  try {
    const accountLink = await stripe.v2.core.accountLinks.create({
      account: accountId,
      use_case: {
        type: 'account_onboarding',
        account_onboarding: {
          configurations: ['merchant', 'customer'],
          refresh_url: `${process.env.DOMAIN}`,
          return_url: `\({process.env.DOMAIN}?accountId=\){accountId}`,
        },
      },
    });
    res.json({ url: accountLink.url });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(500).json({ error: message });
  }
});
</code></pre>
<p>The <code>accountLinks.create()</code> method generates a temporary URL that takes the merchant to Stripe’s onboarding form. On that form, Stripe collects the merchant’s identity documents, bank account details, and tax information. You don't need to build any of this yourself.</p>
<p>The <code>return_url</code> is where Stripe redirects the merchant after they complete onboarding. Notice that you append the <code>accountId</code> as a query parameter so your frontend can pick it up and store it.</p>
<h2 id="heading-how-to-check-account-status"><strong>How to Check Account Status</strong></h2>
<p>You need a way to check whether a merchant has finished onboarding and is ready to accept payments. Add this route:</p>
<pre><code class="language-typescript">// Get Connected Account Status
router.get(
  '/account-status/:accountId',
  async (req: Request&lt;{ accountId: string }&gt;, res: Response) =&gt; {
    try {
      const account = await stripe.v2.core.accounts.retrieve(req.params.accountId, {
        include: ['requirements', 'configuration.merchant'],
      });
      const payoutsEnabled =
        account.configuration?.merchant?.capabilities?.stripe_balance?.payouts?.status === 'active';
      const chargesEnabled =
        account.configuration?.merchant?.capabilities?.card_payments?.status === 'active';
      const summaryStatus = account.requirements?.summary?.minimum_deadline?.status;
      const detailsSubmitted = !summaryStatus || summaryStatus === 'eventually_due';
      res.json({
        id: account.id,
        payoutsEnabled,
        chargesEnabled,
        detailsSubmitted,
        requirements: account.requirements?.entries,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      res.status(500).json({ error: message });
    }
  },
);
</code></pre>
<p>This route retrieves the connected account and checks three important statuses:</p>
<ul>
<li><p><code>chargesEnabled</code> tells you if the merchant can accept payments.</p>
</li>
<li><p><code>payoutsEnabled</code> tells you if they can receive payouts to their bank account.</p>
</li>
<li><p><code>detailsSubmitted</code> tells you if they have completed the onboarding form.</p>
</li>
</ul>
<p>Your frontend will use these flags to show or hide features.</p>
<h2 id="heading-how-to-create-products-through-stripe"><strong>How to Create Products Through Stripe</strong></h2>
<p>Instead of storing products in a database, you'll create them directly in Stripe. Each product is created on the merchant’s connected account using the <code>stripeAccount</code> header. This means each merchant has their own isolated product catalog inside Stripe.</p>
<pre><code class="language-typescript">// Type definition for product creation
interface CreateProductBody {
  productName: string;
  productDescription: string;
  productPrice: number;
  accountId: string;
}
// Create a product on the connected account
router.post('/create-product', async (req: Request&lt;{}, {}, CreateProductBody&gt;, res: Response) =&gt; {
  const { productName, productDescription, productPrice, accountId } = req.body;
  try {
    // Create the product on the connected account
    const product = await stripe.products.create(
      {
        name: productName,
        description: productDescription,
      },
      { stripeAccount: accountId },
    ); // Create a price for the product
    const price = await stripe.prices.create(
      {
        product: product.id,
        unit_amount: productPrice,
        currency: 'usd',
      },
      { stripeAccount: accountId },
    );
    res.json({
      productName,
      productDescription,
      productPrice,
      priceId: price.id,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(500).json({ error: message });
  }
});
</code></pre>
<p>There are two Stripe API calls happening here. First, <code>stripe.products.create()</code> creates the product (name and description). Then <code>stripe.prices.create()</code> creates a price for that product (amount and currency).</p>
<p>Stripe separates products from prices because a single product can have multiple prices — for example, a monthly plan and an annual plan.</p>
<p>The <code>{ stripeAccount: accountId }</code> option on both calls tells Stripe to create these resources on the merchant’s connected account, not on your platform account. This is a critical detail: without it, the products would be created on your platform’s account and the merchant would never see them.</p>
<h2 id="heading-how-to-fetch-products"><strong>How to Fetch Products</strong></h2>
<p>Add a route to list all products for a given merchant:</p>
<pre><code class="language-typescript">// Fetch products for a specific account
router.get('/products/:accountId', async (req: Request&lt;{ accountId: string }&gt;, res: Response) =&gt; {
  const { accountId } = req.params;
  try {
    const options: Stripe.RequestOptions = {};
    if (accountId !== 'platform') {
      options.stripeAccount = accountId;
    }
    const prices = await stripe.prices.list(
      {
        expand: ['data.product'],
        active: true,
        limit: 100,
      },
      options,
    );
    const products = prices.data.map((price) =&gt; {
      const product = price.product as Stripe.Product;
      return {
        id: product.id,
        name: product.name,
        description: product.description,
        price: price.unit_amount,
        priceId: price.id,
        period: price.recurring ? price.recurring.interval : null,
      };
    });
    res.json(products);
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(500).json({ error: message });
  }
});
</code></pre>
<p>This route fetches all active prices from a merchant’s Stripe account and expands the product data (using <code>expand: ["data.product"]</code>) so you get the product name and description in the same API call. The period field will be null for one-time products and "month" or "year" for subscriptions.</p>
<h2 id="heading-how-to-build-the-checkout-flow"><strong>How to Build the Checkout Flow</strong></h2>
<p>Your checkout flow needs to handle two scenarios: one-time payments for individual products, and recurring subscriptions. Stripe’s Checkout Sessions handle both — you just need to set the mode based on the price type.</p>
<pre><code class="language-typescript">// Type definition for checkout
interface CheckoutBody {
  priceId: string;
  accountId: string;
}
// Create checkout session
router.post(
  '/create-checkout-session',
  async (req: Request&lt;{}, {}, CheckoutBody&gt;, res: Response) =&gt; {
    const { priceId, accountId } = req.body;
    try {
      // Retrieve the price to determine if it is
      // one-time or recurring
      const price = await stripe.prices.retrieve(priceId, { stripeAccount: accountId });
      const isSubscription = price.type === 'recurring';
      const mode = isSubscription ? 'subscription' : 'payment';
      const session = await stripe.checkout.sessions.create(
        {
          line_items: [
            {
              price: priceId,
              quantity: 1,
            },
          ],
          mode,
          success_url: `${process.env.DOMAIN}/done?session_id={CHECKOUT_SESSION_ID}`,
          cancel_url: `${process.env.DOMAIN}`,
          ...(isSubscription
            ? {
                subscription_data: {
                  application_fee_percent: 10,
                },
              }
            : {
                payment_intent_data: {
                  application_fee_amount: 123,
                },
              }),
        },
        { stripeAccount: accountId },
      );
      res.redirect(303, session.url as string);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error';
      res.status(500).json({ error: message });
    }
  },
);
</code></pre>
<p>Here's what this route does step by step. First, it retrieves the price from the merchant’s connected account to check whether it is a one-time price or a recurring subscription. Then it creates a Checkout Session with the appropriate mode — either "payment" or "subscription".</p>
<p>The <code>application_fee_amount</code> is your platform’s cut of the transaction, specified in the smallest currency unit (cents for USD). In this example, you take $1.23 or 10% per transaction. For a real marketplace, you would likely calculate this as a percentage of the product price.</p>
<p>Notice that <code>application_fee_amount</code> goes inside <code>subscription_data</code> for subscriptions but inside <code>payment_intent_data</code> for one-time payments. This is a Stripe requirement — the two modes use different configuration objects.</p>
<p>Finally, the route uses <code>res.redirect(303, session.url)</code> to send the customer directly to Stripe’s hosted checkout page.</p>
<h2 id="heading-how-to-handle-webhooks"><strong>How to Handle Webhooks</strong></h2>
<p>Webhooks are how Stripe tells your server about events that happen asynchronously — like a successful payment, a failed charge, or a subscription cancellation.</p>
<p>In a production marketplace, you should <strong>never</strong> rely solely on redirect URLs to confirm payments. A customer might close their browser before the redirect completes. Webhooks are your source of truth.</p>
<p>Add the webhook endpoint <strong>before</strong> the JSON body parser. Stripe sends webhook payloads as raw bytes, and you need the raw body to verify the signature:</p>
<pre><code class="language-typescript">// IMPORTANT: Register this BEFORE app.use(express.json())
app.post(
  '/api/webhook',
  express.raw({ type: 'application/json' }),
  (req: Request, res: Response) =&gt; {
    let event: Stripe.Event = JSON.parse(req.body.toString()); // If you have an endpoint secret, verify the
    // signature for security
    const endpointSecret = process.env.WEBHOOK_SECRET;
    if (endpointSecret) {
      const signature = req.headers['stripe-signature'] as string;
      try {
        event = stripe.webhooks.constructEvent(req.body, signature, endpointSecret) as Stripe.Event;
      } catch (err) {
        const message = err instanceof Error ? err.message : 'Unknown error';
        console.log('Webhook signature verification failed:', message);
        res.sendStatus(400);
        return;
      }
    } // Handle the event
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;
        console.log('Payment successful for session:', session.id); // Fulfill the order: send email, grant access,
        // update your records, and so on
        break;
      }
      case 'checkout.session.expired': {
        const session = event.data.object as Stripe.Checkout.Session;
        console.log('Session expired:', session.id); // Optionally notify the customer or clean up
        // any pending records
        break;
      }
      case 'checkout.session.async_payment_succeeded': {
        const session = event.data.object as Stripe.Checkout.Session;
        console.log('Delayed payment succeeded for session:', session.id); // Fulfill the order now that payment cleared
        break;
      }
      case 'checkout.session.async_payment_failed': {
        const session = event.data.object as Stripe.Checkout.Session;
        console.log('Payment failed for session:', session.id); // Notify the customer that payment failed
        break;
      }
      case 'customer.subscription.deleted': {
        const subscription = event.data.object as Stripe.Subscription;
        console.log('Subscription cancelled:', subscription.id); // Revoke access for the customer
        break;
      }
      default:
        console.log('Unhandled event type:', event.type);
    }
    res.send();
  },
);
</code></pre>
<p>The webhook handler checks for five key events.</p>
<ul>
<li><p><code>checkout.session.completed</code> fires when a payment succeeds — this is where you would fulfill an order, send a confirmation email, or grant access.</p>
</li>
<li><p><code>checkout.session.expired</code> fires when a session expires before the customer completes payment.</p>
</li>
<li><p><code>checkout.session.async_payment_succeeded</code> fires when a delayed payment method (like a bank transfer) finally goes through.</p>
</li>
<li><p><code>checkout.session.async_payment_failed</code> fires when a delayed payment method fails.</p>
</li>
<li><p>And <code>customer.subscription.deleted</code> fires when a subscription is cancelled.</p>
</li>
</ul>
<h2 id="heading-how-to-configure-webhooks-in-the-stripe-dashboard"><strong>How to Configure Webhooks in the Stripe Dashboard</strong></h2>
<p>Before you can receive webhook events, you need to tell Stripe where to send them and which events you care about. Follow these steps:</p>
<ol>
<li><p>Go to the Stripe Dashboard and navigate to Developers &gt; Webhooks.</p>
</li>
<li><p>Click "Add destination."</p>
</li>
<li><p>Under the account type, select "Connected and V2 accounts" since your payments go through connected merchant accounts.</p>
</li>
<li><p>Under "Events to listen for," click "All events" and select the following five events:</p>
<ul>
<li><p><code>checkout.session.async_payment_succeeded</code> — Occurs when a payment intent using a delayed payment method finally succeeds.</p>
</li>
<li><p><code>checkout.session.completed</code> — Occurs when a Checkout Session has been successfully completed.</p>
</li>
<li><p><code>checkout.session.expired</code> — Occurs when a Checkout Session expires before completion.</p>
</li>
<li><p><code>checkout.session.async_payment_failed</code> — Occurs when a payment intent using a delayed payment method fails.</p>
</li>
<li><p><code>customer.subscription.deleted</code> — Occurs whenever a customer’s subscription ends.</p>
</li>
</ul>
</li>
<li><p>Enter your webhook endpoint URL. For production, this would be something like <a href="https://yourdomain.com/api/webhook">https://yourdomain.com/api/webhook</a>. For local development, you will use the Stripe CLI instead (covered next).</p>
</li>
<li><p>Click "Add destination" to save.</p>
</li>
</ol>
<h2 id="heading-how-to-test-webhooks-locally"><strong>How to Test Webhooks Locally</strong></h2>
<p>For local development, you don't need to expose your server to the internet. Install the Stripe CLI and run:</p>
<pre><code class="language-shell">brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:4242/webhook
</code></pre>
<p>The CLI will print a webhook signing secret that starts with <code>whsec_</code>. Add this to your .env file as <code>WEBHOOK_SECRET</code>. The CLI intercepts all webhook events from Stripe and forwards them to your local server, so you can test the full payment flow without deploying anything.</p>
<h2 id="heading-how-to-add-the-billing-portal"><strong>How to Add the Billing Portal</strong></h2>
<p>The billing portal lets customers manage their subscriptions without you building any UI for it. Stripe hosts the entire experience — customers can update their payment method, change plans, or cancel their subscription.</p>
<pre><code class="language-typescript">// Create a billing portal session
router.post(
&nbsp; "/create-portal-session",
&nbsp; async (req: Request, res: Response) =&gt; {
&nbsp;&nbsp;&nbsp; const { session_id } = req.body as {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; session_id: string;
&nbsp;&nbsp;&nbsp; };
&nbsp;
&nbsp;&nbsp;&nbsp; try {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const session =
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; await stripe.checkout.sessions.retrieve(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; session_id
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; );
&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const portalSession =
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; await stripe.billingPortal.sessions.create({
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; customer_account: session.customer_account as string,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return_url: `\({process.env.DOMAIN}?session_id=\){session_id}`,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; });
&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; res.redirect(303, portalSession.url);
&nbsp;&nbsp;&nbsp; } catch (error) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; const message =
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; error instanceof Error
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ? error.message
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; : "Unknown error";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; res.status(500).json({ error: message });
&nbsp;&nbsp;&nbsp; }
&nbsp; }
);
</code></pre>
<p>This route takes a <code>session_id</code> from a previous checkout, retrieves the associated customer, and creates a billing portal session. The <code>customer_account</code> field links the portal to the correct connected account so the customer sees only their subscriptions with that specific merchant.</p>
<p>Now add the JSON parser and mount the router. This must come <strong>after</strong> the webhook route:</p>
<pre><code class="language-typescript">// JSON and URL-encoded parsers (AFTER webhook route)
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Mount all routes under /api
app.use('/api', router);
const PORT: number = parseInt(process.env.PORT || '4242', 10);
app.listen(PORT, () =&gt; {
  console.log(`Server running on port ${PORT}`);
});
</code></pre>
<h2 id="heading-how-to-build-the-nextjs-frontend"><strong>How to Build the Next.js Frontend</strong></h2>
<p>Navigate to the client directory and create a new Next.js project with TypeScript:</p>
<pre><code class="language-shell">cd ../client
npx create-next-app@latest . --typescript --app --tailwind --eslint
npm install axios
</code></pre>
<h2 id="heading-how-to-create-the-account-context"><strong>How to Create the Account Context</strong></h2>
<p>You need a way to share the merchant’s account ID across all components. Create a context provider at <code>contexts/AccountContext.tsx</code>:</p>
<pre><code class="language-typescript">'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
import { useSearchParams } from 'next/navigation';

interface AccountContextType {
  accountId: string | null;
  setAccountId: (id: string | null) =&gt; void;
}

const AccountContext = createContext&lt;AccountContextType | undefined&gt;(undefined);

export function useAccount(): AccountContextType {
  const context = useContext(AccountContext);
  if (!context) {
    throw new Error('useAccount must be used within AccountProvider');
  }
  return context;
}

export function AccountProvider({ children }: { children: ReactNode }) {
  const searchParams = useSearchParams();
  const [accountId, setAccountId] = useState&lt;string | null&gt;(searchParams.get('accountId'));

  return (
    &lt;AccountContext.Provider value={{ accountId, setAccountId }}&gt;
      {children}
    &lt;/AccountContext.Provider&gt;
  );
}
</code></pre>
<p>This context stores the current merchant’s account ID and makes it available throughout the app. On initial load, it checks the URL for an accountId query parameter — this is how Stripe’s onboarding redirect passes the account ID back to your app.</p>
<h2 id="heading-how-to-create-the-account-status-hook"><strong>How to Create the Account Status Hook</strong></h2>
<p>Create a custom hook at <code>hooks/useAccountStatus.ts</code> that polls the account status:</p>
<pre><code class="language-typescript">'use client';
import { useState, useEffect } from 'react';
import { useAccount } from '@/contexts/AccountContext';
interface AccountStatus {
  id: string;
  payoutsEnabled: boolean;
  chargesEnabled: boolean;
  detailsSubmitted: boolean;
}
export default function useAccountStatus() {
  const [accountStatus, setAccountStatus] = useState&lt;AccountStatus | null&gt;(null);
  const { accountId, setAccountId } = useAccount();
  useEffect(() =&gt; {
    if (!accountId) return;
    const fetchStatus = async () =&gt; {
      try {
        const res = await fetch(`http://localhost:4242/api/account-status/${accountId}`);
        if (!res.ok) throw new Error('Failed to fetch');
        const data: AccountStatus = await res.json();
        setAccountStatus(data);
      } catch {
        setAccountId(null);
      }
    };
    fetchStatus();
    const interval = setInterval(fetchStatus, 5000);
    return () =&gt; clearInterval(interval);
  }, [accountId, setAccountId]);
  return {
    accountStatus,
    needsOnboarding: !accountStatus?.chargesEnabled &amp;&amp; !accountStatus?.detailsSubmitted,
  };
}
</code></pre>
<p>This hook polls the account status every 5 seconds. This is important because Stripe’s onboarding is asynchronous — a merchant might complete the form, but it can take a moment for Stripe to verify their details and activate their account. The <code>needsOnboarding</code> flag tells your UI whether to show the onboarding button or the merchant dashboard.</p>
<h2 id="heading-how-to-build-the-merchant-onboarding-component"><strong>How to Build the Merchant Onboarding Component</strong></h2>
<p>Create <code>components/ConnectOnboarding.tsx</code>:</p>
<pre><code class="language-typescript">'use client';
import { useState } from 'react';
import { useAccount } from '@/contexts/AccountContext';
import useAccountStatus from '@/hooks/useAccountStatus';
const API_URL = 'http://localhost:4242/api';
export default function ConnectOnboarding() {
  const [email, setEmail] = useState&lt;string&gt;('');
  const { accountId, setAccountId } = useAccount();
  const { accountStatus, needsOnboarding } = useAccountStatus();
  const handleCreateAccount = async () =&gt; {
    const res = await fetch(`${API_URL}/create-connect-account`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    });
    const data = await res.json();
    setAccountId(data.accountId);
  };
  const handleStartOnboarding = async () =&gt; {
    const res = await fetch(`${API_URL}/create-account-link`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ accountId }),
    });
    const data = await res.json();
    window.location.href = data.url;
  };
  if (!accountId) {
    return (
      &lt;div className="max-w-md mx-auto p-6"&gt;
        &lt;h2 className="text-xl font-bold mb-4"&gt;Create Your Seller Account&lt;/h2&gt;
        &lt;input
          type="email"
          placeholder="Your email"
          value={email}
          onChange={(e) =&gt; setEmail(e.target.value)}
          className="w-full border p-2 rounded mb-4"
        /&gt;
        &lt;button
          onClick={handleCreateAccount}
          className="w-full bg-green-600 text-white p-2 rounded hover:bg-green-700"
        &gt;
          Create Connect Account
        &lt;/button&gt;
      &lt;/div&gt;
    );
  }
  return (
    &lt;div className="max-w-md mx-auto p-6"&gt;
      &lt;h3 className="font-semibold mb-2"&gt;Account: {accountId} &lt;/h3&gt;
      &lt;p className="mb-2"&gt;Charges: {accountStatus?.chargesEnabled ? 'Active' : 'Pending'} &lt;/p&gt;
      &lt;p className="mb-4"&gt;Payouts: {accountStatus?.payoutsEnabled ? 'Active' : 'Pending'} &lt;/p&gt;
      {needsOnboarding &amp;&amp; (
        &lt;button
          onClick={handleStartOnboarding}
          className="bg-purple-600 text-white px-6 py-2 rounded hover:bg-purple-700"
        &gt;
          Complete Onboarding
        &lt;/button&gt;
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<p>This component handles both states of the merchant experience. If no account exists, it shows a simple email form. After account creation, it shows the account status and an onboarding button if needed.</p>
<h2 id="heading-how-to-build-the-product-create-product-list-and-checkout"><strong>How to Build the Product Create, Product List and Checkout</strong></h2>
<p>Create <code>components/Products.tsx</code>:</p>
<pre><code class="language-typescript">'use client';
import { useState, useEffect } from 'react';
import { useAccount } from '@/contexts/AccountContext';
import useAccountStatus from '@/hooks/useAccountStatus';
const API_URL = 'http://localhost:4242/api';
interface Product {
  id: string;
  name: string;
  description: string | null;
  price: number | null;
  priceId: string;
  period: string | null;
}
export default function Products() {
  const { accountId } = useAccount();
  const { needsOnboarding } = useAccountStatus();
  const [products, setProducts] = useState&lt;Product[]&gt;([]);
  useEffect(() =&gt; {
    if (!accountId || needsOnboarding) return;
    const fetchProducts = async () =&gt; {
      const res = await fetch(`\({API_URL}/products/\){accountId}`);
      const data: Product[] = await res.json();
      setProducts(data);
    };
    fetchProducts();
    const interval = setInterval(fetchProducts, 5000);
    return () =&gt; clearInterval(interval);
  }, [accountId, needsOnboarding]);
  return (
    &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6"&gt;
      {' '}
      {products.map((product) =&gt; (
        &lt;div key={product.priceId} className="border rounded-lg p-4 shadow-sm"&gt;
          &lt;h3 className="text-lg font-semibold"&gt;&nbsp; {product.name}&lt;/h3&gt;

          &lt;p className="text-gray-600 mt-1"&gt;&nbsp; {product.description}&lt;/p&gt;

          &lt;p className="text-xl font-bold mt-3"&gt;
            ${((product.price ?? 0) / 100).toFixed(2)}
            {product.period ? ` / ${product.period}` : ''}
          &lt;/p&gt;

          &lt;form action={`${API_URL}/create-checkout-session`} method="POST"&gt;
            &lt;input type="hidden" name="priceId" value={product.priceId} /&gt;
            &lt;input type="hidden" name="accountId" value={accountId ?? ''} /&gt;
            &lt;button
              type="submit"
              className="mt-4 w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
            &gt;
              {product.period ? 'Subscribe' : 'Buy Now'}
            &lt;/button&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
}

</code></pre>
<p>The Products component fetches all products from the merchant’s Stripe account and displays them in a responsive grid. The checkout button submits a form directly to your backend, which redirects the customer to Stripe’s hosted checkout page. Notice how the button text changes based on whether the product is a one-time purchase or a subscription.</p>
<h2 id="heading-how-to-build-the-product-form"><strong>How to Build the Product Form</strong></h2>
<p>Merchants need a way to add products from the frontend. Create <code>components/ProductForm.tsx</code>:</p>
<pre><code class="language-typescript">'use client';
import { useState } from 'react';
import { useAccount } from '@/contexts/AccountContext';
import useAccountStatus from '@/hooks/useAccountStatus';
const API_URL = 'http://localhost:4242/api';
interface ProductFormData {
  productName: string;
  productDescription: string;
  productPrice: number;
}
export default function ProductForm() {
  const { accountId } = useAccount();
  const { needsOnboarding } = useAccountStatus();
  const [showForm, setShowForm] = useState&lt;boolean&gt;(false);
  const [formData, setFormData] = useState&lt;ProductFormData&gt;({
    productName: '',
    productDescription: '',
    productPrice: 1000,
  });
  const handleSubmit = async (e: React.FormEvent): Promise&lt;void&gt; =&gt; {
    e.preventDefault();
    if (!accountId || needsOnboarding) return;
    await fetch(`${API_URL}/create-product`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...formData,
        accountId,
      }),
    }); // Reset form and hide it
    setFormData({
      productName: '',
      productDescription: '',
      productPrice: 1000,
    });
    setShowForm(false);
  }; // Only show the form if the merchant has completed
  // onboarding and can accept charges
  if (!accountId || needsOnboarding) return null;
  return (
    &lt;div className="my-6"&gt;
      &lt;button
        onClick={() =&gt; setShowForm(!showForm)}
        className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
      &gt;
        {showForm ? 'Cancel' : 'Add New Product'}
      &lt;/button&gt;

      {showForm &amp;&amp; (
        &lt;form onSubmit={handleSubmit} className="mt-4 max-w-md space-y-4"&gt;
          &lt;div&gt;
            &lt;label className="block text-sm font-medium mb-1"&gt;Product Name&lt;/label&gt;

            &lt;input
              type="text"
              value={formData.productName}
              onChange={(e) =&gt;
                setFormData({
                  ...formData,
                  productName: e.target.value,
                })
              }
              className="w-full border p-2 rounded"
              required
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;label className="block text-sm font-medium mb-1"&gt;Description&lt;/label&gt;
            &lt;input
              type="text"
              value={formData.productDescription}
              onChange={(e) =&gt;
                setFormData({
                  ...formData,
                  productDescription: e.target.value,
                })
              }
              className="w-full border p-2 rounded"
            /&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;label className="block text-sm font-medium mb-1"&gt;Price (in cents)&lt;/label&gt;

            &lt;input
              type="number"
              value={formData.productPrice}
              onChange={(e) =&gt;
                setFormData({
                  ...formData,
                  productPrice: parseInt(e.target.value),
                })
              }
              className="w-full border p-2 rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          &gt;
            Create Product
          &lt;/button&gt;
        &lt;/form&gt;
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<p>This component only renders after the merchant has completed onboarding (the <code>if (!accountId || needsOnboarding) return null</code> check at the top). It toggles a form where the merchant enters a product name, description, and price in cents. When submitted, it calls your <code>/api/create-product</code> endpoint, which creates both the product and its price on the merchant’s connected Stripe account.</p>
<p>The price field uses cents because that is what Stripe expects. So if a merchant wants to sell a product for \(25.00, they enter 2500. In a production app, you would add a friendlier input that lets merchants type \)25.00 and converts it to cents automatically.</p>
<h2 id="heading-how-to-build-the-main-page"><strong>How to Build the Main Page</strong></h2>
<p>Finally, put it all together in <code>app/page.tsx</code>:</p>
<pre><code class="language-typescript">'use client';
import { AccountProvider } from '@/contexts/AccountContext';
import ConnectOnboarding from '@/components/ConnectOnboarding';
import Products from '@/components/Products';
import ProductForm from '@/components/ProductForm';
export default function Home() {
  return (
    &lt;AccountProvider&gt;
      {' '}
      &lt;main className="max-w-6xl mx-auto p-8"&gt;
        &lt;h1 className="text-3xl font-bold mb-8"&gt; Marketplace Dashboard &lt;/h1&gt;
        &lt;ConnectOnboarding /&gt;
        &lt;ProductForm /&gt;
        &lt;Products /&gt;
      &lt;/main&gt;
    &lt;/AccountProvider&gt;
  );
}
</code></pre>
<h2 id="heading-how-to-test-the-full-flow"><strong>How to Test the Full Flow</strong></h2>
<p>Start both servers:</p>
<pre><code class="language-shell"># Terminal 1 - Backend
cd server
npm run dev
&nbsp;
# Terminal 2 - Frontend
cd client
npm run dev
&nbsp;
# Terminal 3 - Stripe webhook listener
stripe listen --forward-to localhost:4242/api/webhook
</code></pre>
<p>Now test the complete flow:</p>
<ol>
<li><p>Go to <a href="http://localhost:3000">http://localhost:3000</a> and enter an email to create a merchant account.</p>
</li>
<li><p>Click "Complete Onboarding" and fill out Stripe’s test onboarding form. Use test data like 000-000-0000 for the phone number and 0000 for the last four digits of SSN.</p>
</li>
<li><p>Wait a few seconds for the account status to update. Once charges are active, you can add products.</p>
</li>
<li><p>Create a product using the product form (set the price in cents — for example, 2500 for $25.00).</p>
</li>
<li><p>Click "Buy Now" on a product to start the checkout flow.</p>
</li>
<li><p>On Stripe’s checkout page, use the test card number 4242 4242 4242 4242 with any future expiry date and any CVC.</p>
</li>
<li><p>Check your terminal — you should see the webhook event confirming the payment.</p>
</li>
<li><p>Check the Stripe Dashboard to see the payment, the application fee, and the transfer to the connected account.</p>
</li>
</ol>
<h2 id="heading-how-the-payment-split-works"><strong>How the Payment Split Works</strong></h2>
<p>Here is exactly what happens when a customer pays $25.00 for a product:</p>
<ol>
<li><p>The customer pays $25.00 on Stripe’s checkout page.</p>
</li>
<li><p>Stripe deducts its processing fee (approximately 2.9% + $0.30 for US cards).</p>
</li>
<li><p>Your platform takes the application fee you set ($1.23 in our example).</p>
</li>
<li><p>The remaining amount is transferred to the merchant’s connected Stripe account.</p>
</li>
<li><p>The merchant can withdraw their funds to their bank account from the Stripe Dashboard.</p>
</li>
</ol>
<p>You control the application fee in the checkout route. In a production marketplace, you would calculate this as a percentage of the transaction. For example, to take a 10% fee:</p>
<pre><code class="language-plaintext">onst applicationFee = Math.round(
&nbsp; (price.unit_amount ?? 0) * 0.1
);
</code></pre>
<h2 id="heading-next-steps"><strong>Next Steps</strong></h2>
<p>You now have a working marketplace. Here are improvements to consider for production:</p>
<ul>
<li><p>Add authentication with NextAuth.js so merchants can securely log in and manage their accounts across sessions.</p>
</li>
<li><p>Add runtime validation with Zod to validate all request bodies before they reach Stripe.</p>
</li>
<li><p>Add image uploads for products using Cloudinary or AWS S3, then pass the image URL to Stripe’s product metadata.</p>
</li>
<li><p>Build separate merchant and customer views. Right now the app combines both experiences on one page.</p>
</li>
<li><p>Deploy your backend to Railway or Render and your frontend to Vercel. Update the webhook URL in your Stripe Dashboard to point to your production server.</p>
</li>
</ul>
<p>You can find the complete source code for this tutorial on GitHub: <a href="https://github.com/michaelokolo/marketplace">https://github.com/michaelokolo/marketplace</a></p>
<h2 id="heading-acknowledgements"><strong>Acknowledgements</strong></h2>
<p>Some API usage patterns in this tutorial are inspired by examples from the <a href="https://docs.stripe.com">official Stripe documentation</a>. These examples were adapted to demonstrate how to build a complete multi-vendor marketplace architecture.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this handbook, you built a complete online marketplace where merchants can onboard through Stripe Connect, create products stored directly in Stripe, and receive payments from customers — all without a traditional database.</p>
<p>You learned how to use Stripe’s V2 Accounts API for merchant onboarding, create products and prices on connected accounts, build a checkout flow that handles both one-time payments and subscriptions, listen for payment events with webhooks, and give customers a billing portal to manage their subscriptions.</p>
<p>The key insight is that Stripe Connect handles the hardest parts of running a marketplace — payment splitting, tax compliance, identity verification, and fund transfers. Your job is to build a great user experience on top of it.</p>
<p>If you found this tutorial helpful, share it with someone who is learning to build full-stack applications. Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Production Ready eCommerce Website with ReactJS, TailwindCSS, PlanetScale and Stripe ]]>
                </title>
                <description>
                    <![CDATA[ Hello, welcome to this tutorial. Today we're going to build a production-ready eCommerce website using ReactJS, TailwindCSS, PlanetScale, and Stripe. Before we begin, you should be familiar with the basics of React.js and Next.js to get the most out ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-ecommerce-website-using-next-js-and-planetscale/</link>
                <guid isPermaLink="false">66d460ef4a0edd9b48e83583</guid>
                
                    <category>
                        <![CDATA[ ecommerce ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ stripe ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tailwind ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sharvin Shah ]]>
                </dc:creator>
                <pubDate>Tue, 25 Oct 2022 16:30:14 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/10/Add-a-heading.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hello, welcome to this tutorial. Today we're going to build a production-ready eCommerce website using ReactJS, TailwindCSS, PlanetScale, and Stripe.</p>
<p>Before we begin, you should be familiar with the basics of React.js and Next.js to get the most out of this guide.</p>
<p>If you're not and need to brush up, I recommend you go through the <a target="_blank" href="https://reactjs.org/docs/getting-started.html">ReactJS</a> and <a target="_blank" href="https://nextjs.org/docs/getting-started">NextJS documentation</a>.</p>
<h2 id="heading-the-stack-we-will-use">The stack we will use:</h2>
<ol>
<li><p><a target="_blank" href="https://reactjs.org/docs/getting-started.html">ReactJS</a> is a JavaScript library for building user interfaces. It is declarative and component-based.</p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/docs/getting-started">NextJS</a> is a React-based framework that lets us render data on the server side. It helps Google crawl the application which results in SEO benefits.</p>
</li>
<li><p><a target="_blank" href="https://planetscale.com/docs">PlanetScale</a> is a database as a service that is developed on Vitess, an open-source technology that powers YouTube and uses MySQL internally.</p>
</li>
<li><p><a target="_blank" href="https://tailwindcss.com/">TailwindCSS</a> is a utility-first CSS framework packed with classes that can be composed to build any design, directly in our markup.</p>
</li>
<li><p><a target="_blank" href="https://www.prisma.io/docs/">Prisma</a> is an ORM built for NodeJS and TypeScript which handles automated migrations, type-safety, and auto-completion.</p>
</li>
<li><p><a target="_blank" href="https://vercel.com/docs">Vercel</a> will host our application. It scales well, all without any configuration, and deployment is instant.</p>
</li>
<li><p><a target="_blank" href="https://stripe.com">Stripe</a> is a payment gateway, and we will use it to accept online payments on the website.</p>
</li>
</ol>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#how-to-configure-planetscale-prisma-nextjs-and-stripe-">How to Configure PlanetScale, Stripe, NextJS, Prisma and Other Libraries</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-mock-data-category-products-api-and-all-category-single-category-ui">How to Implement Mock Data, Category-Products API and All Category-Single Category UI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-single-product-ui-and-stripe-checkout">How to Implement Single Product UI and Stripe Checkout</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-the-website-to-production">How to Deploy the Website to Production</a></p>
</li>
</ol>
<p>I am going to divide this tutorial into four separate sections.</p>
<p>At the start of every section, you will find a Git commit that has the code developed in that section. Also, if you want to see the complete code, then it is available in this <a target="_blank" href="https://github.com/Sharvin26/Ecommerce-Website-ReactJS-TailwindCSS-PlanetScale-Stripe">repository</a>.</p>
<h2 id="heading-how-to-configure-planetscale-stripe-nextjs-tailwindcss-and-prisma">How to Configure PlanetScale, Stripe, NextJS, TailwindCSS, and Prisma.</h2>
<p>In this section, we'll implement the following functionality:</p>
<ol>
<li><p>Create a PlanetScale Account and Database.</p>
</li>
<li><p>Create a Stripe Account.</p>
</li>
<li><p>Configure NextJS, TailwindCSS, and Prisma.</p>
</li>
</ol>
<p>You can find the <strong>eCommerce</strong> website <strong>code</strong> implemented in this section at this <a target="_blank" href="https://github.com/Sharvin26/Ecommerce-Website-ReactJS-TailwindCSS-PlanetScale-Stripe/tree/afa389dc07f565a39eacac5e3801fcc4e8d9041f">commit</a>.</p>
<h3 id="heading-how-to-configure-planetscale">How to Configure PlanetScale:</h3>
<p>To create a PlanetScale account, visit this <a target="_blank" href="https://planetscale.com/">URL</a>. Click on Get started button at the top right corner.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.01.59-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Landing Page</em></p>
<p>You can either create an account using GitHub or a traditional email-password. Once the account is created, then click on the "create" link.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-05-at-5.00.59-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Dashboard Page</em></p>
<p>You'll receive the following modal:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-05-at-5.08.12-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale New Database Modal</em></p>
<p>Fill in the details and click on the Create database button. Once the database is created you'll be redirected to the following page:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.06.05-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Ecommerce Website Database Page</em></p>
<p>Click on connect and a modal will open. This modal will contain a Database URL and this password cannot be generated again. So copy and paste it into a safe location.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.07.27-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Database Username and Password Modal</em></p>
<h3 id="heading-how-to-configure-stripe">How to Configure Stripe:</h3>
<p>To create a Stripe account, go to this <a target="_blank" href="https://dashboard.stripe.com/register">URL</a>. Once you've created the account, click on the Developer Button from the Nav menu. You'll see API keys on the left side and you'll find the Publishable key and Secret key under Standard keys.</p>
<p>Publishable key: These are the keys that can be publicly-accessible in a web or mobile app’s client-side code.</p>
<p>Secret key: This is a secret credential and should be securely stored in the server code. This key is used to call the Stripe API.</p>
<h3 id="heading-how-to-configure-nextjs-tailwindcss-and-prisma">How to Configure NextJS, TailwindCSS, and Prisma.</h3>
<p>First, we will create a NextJS app using the following command:</p>
<pre><code class="lang-shell">npx create-next-app ecommerce-tut --ts --use-npm
</code></pre>
<p>Once the project is created, open it with your favourite editor. You'll get the following structure:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-05-at-6.03.07-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Project Structure</em></p>
<p>Let's create a directory named <code>src</code>. We will move the <code>pages</code> and <code>styles</code> directory to that <code>src</code> folder. You'll get the following structure:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.28.16-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Project Structure after moving Pages and Styles.</em></p>
<p>Install the following packages:</p>
<pre><code class="lang-shell">npm i @ngneat/falso @prisma/client @stripe/stripe-js @tanstack/react-query currency.js next-connect react-icons react-intersection-observer stripe
</code></pre>
<p>We also need to install dev dependencies:</p>
<pre><code class="lang-shell">npm i --save-dev @tanstack/react-query-devtools autoprefixer postcss tailwindcss
</code></pre>
<p>Let's understand each of the packages:</p>
<ol>
<li><p><a target="_blank" href="https://ngneat.github.io/falso/">@ngneat/falso</a>: We will use this library to create mock data for our eCommerce website. In an ideal world, you would have an admin panel to add the products, but it is not in the scope of this tutorial.</p>
</li>
<li><p><a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client">@prisma/client</a>: We will use this library to connect to our database, run migrations, and do all CRUD operations on the database.</p>
</li>
<li><p><a target="_blank" href="https://stripe.com/docs/js">@stripe/stripe-js</a>: We will use this library to redirect users to the stripe checkout page and process payment.</p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/query/v4/">@tanstack/react-query</a>: We will use this library for managing our asynchronous state, that is caching API responses.</p>
</li>
<li><p><a target="_blank" href="https://currency.js.org/">currency.js</a>: We will use this library for converting our prices to two decimal format.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/next-connect">next-connect</a>: We will use this library for routing purposes on our Next API layer.</p>
</li>
<li><p><a target="_blank" href="https://react-icons.github.io/react-icons/">react-icons</a>: We will use this library for adding icons to our buttons and links.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/react-intersection-observer">react-intersection-observer</a>: Have you seen infinite scrolling on a lot of websites and wondered how it is implemented? We will use this library to implement that based on the viewport.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/stripe">stripe:</a> We will use the Stripe library to connect with Stripe API from our Next API layer.</p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/query/v4/docs/devtools">@tanstack/react-query-devtools</a>: We will use this library as the only dev dependency to view and manage our cache during development time.</p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/tailwindcss">TailwindCSS:</a> We will use this as our CSS library that also requires PostCSS and AutoPrefixer.</p>
</li>
</ol>
<p>Let's configure TailwindCSS into our project using the following command:</p>
<pre><code class="lang-shell">npx tailwindcss init -p
</code></pre>
<p>You'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.29.52-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>TailwindCSS Config Success</em></p>
<p>Now go to <code>tailwind.config.js</code> and update it with the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [
        <span class="hljs-string">"./src/pages/**/*.{js,ts,jsx,tsx}"</span>,
        <span class="hljs-string">"./src/components/**/*.{js,ts,jsx,tsx}"</span>,
    ],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {},
    },
    <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<p>To generate the CSS, Tailwind needs access to all the HTML Elements. We will be writing the UI components under pages and components only, so we pass it under content.</p>
<p>If you need to use any plugins, for example, typography, then you need to add them under the plugins array. If you need to extend the default theme provide by Tailwind, then you need to add it under <code>theme.extend</code> section.</p>
<p>Now go to <code>/src/styles/globals.css</code> and replace the existing code with the following:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>We will add these three directives in our <code>globals.css</code> file. The meaning of each directive is as follows:</p>
<ol>
<li><p>@tailwind base: This injects a base style provided by Tailwind.</p>
</li>
<li><p>@tailwind components: This injects classes and any other classes added by the plugin.</p>
</li>
<li><p>@tailwind utilities: This injects hover, focus, responsive, dark mode and any other utility added by the plugin.</p>
</li>
</ol>
<p>Remove the <code>Home.module.css</code> from <code>src/styles</code> directory and go to <code>src/pages/index.ts</code> and replace the existing code with the following:</p>
<pre><code class="lang-tsx">import type { NextPage } from "next";
import Head from "next/head";

const Home: NextPage = () =&gt; {
    return (
        &lt;div&gt;
            &lt;Head&gt;
                &lt;title&gt;All Products&lt;/title&gt;
                &lt;meta name="description" content="All Products" /&gt;
                &lt;link rel="icon" href="/favicon.ico" /&gt;
            &lt;/Head&gt;

            &lt;main className="container mx-auto"&gt;
                &lt;h1 className="h-1"&gt;Hello&lt;/h1&gt;
            &lt;/main&gt;
        &lt;/div&gt;
    );
};

export default Home;
</code></pre>
<p>When we run the <code>create-next-app</code> command to create the project, it adds some boilerplate code. Here we removed that in some instances while replacing <code>index.ts</code> with an <code>h1</code> and text that says "Hello".</p>
<p>It's time to run the website using the following command:</p>
<pre><code class="lang-shell">npm run dev
</code></pre>
<p>You'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.36.15-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Open <a target="_blank" href="http://localhost:3000/">http://localhost:3000</a> on your browser, and you'll get the following screen with a hello message:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.38.49-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Screen with Hello Message</em></p>
<p>Let's configure Prisma into our project using the following command:</p>
<pre><code class="lang-shell">npx prisma init
</code></pre>
<p>You'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.41.15-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Prisma Successfully Configured Message</em></p>
<p>Under <code>prisma/schema.prisma</code> replace the existing code with the following code:</p>
<pre><code class="lang-python">// This <span class="hljs-keyword">is</span> your Prisma schema file,
// learn more about it <span class="hljs-keyword">in</span> the docs: https://pris.ly/d/prisma-schema

generator client {
  provider        = <span class="hljs-string">"prisma-client-js"</span>
  previewFeatures = [<span class="hljs-string">"referentialIntegrity"</span>]
}

datasource db {
  provider             = <span class="hljs-string">"mysql"</span>
  url                  = env(<span class="hljs-string">"DATABASE_URL"</span>)
  referentialIntegrity = <span class="hljs-string">"prisma"</span>
}

model Category {
  id        String    @id @default(cuid())
  name      String    @unique
  createdAt DateTime  @default(now())
  products  Product[]
}

model Product {
  id          String    @id @default(cuid())
  title       String    @unique
  description String
  price       String
  quantity    Int
  image       String
  createdAt   DateTime  @default(now())
  category    Category? @relation(fields: [categoryId], references: [id])
  categoryId  String?
}
</code></pre>
<p>This file consists of our database source that is MySQL. We are using MySQL because PlanetScale supports MySQL only.</p>
<p>Also, we have created two models that are:</p>
<h3 id="heading-category">Category:</h3>
<ol>
<li><p>name: Every category will have a unique title.</p>
</li>
<li><p>createdAt: The date when a category is added.</p>
</li>
<li><p>products: A foreign relationship with the product model.</p>
</li>
</ol>
<h3 id="heading-product">Product:</h3>
<ol>
<li><p>title: Every product will have a unique title.</p>
</li>
<li><p>description: This is just information about the product.</p>
</li>
<li><p>price: It is of <code>String</code> type because it will hold a decimal value.</p>
</li>
<li><p>quantity: It is of <code>Int</code> type because it will hold a numerical value.</p>
</li>
<li><p>image: Representation of what the product will look like. We will use placeimg for the purpose of this tutorial.</p>
</li>
<li><p>createdAt: The date when a product is added.</p>
</li>
<li><p>category: A foreign relationship with the category model.</p>
</li>
</ol>
<p>We are using <code>cuid()</code> instead of <code>uuid()</code> for the id because they are better for horizontal scaling and sequential lookup performance. Prisma has inbuilt support for CUID. You can read more about it <a target="_blank" href="https://github.com/paralleldrive/cuid">here</a>.</p>
<p>Now time to update our <code>.env</code> file with the following:</p>
<pre><code class="lang-python">DATABASE_URL=

STRIPE_SECRET_KEY=

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
</code></pre>
<p>You'll find the Stripe secret key and publishable key under the dashboard. The database URL is the one that we had copy-pasted earlier and kept in a safe location. Update this <code>.env</code> with those credentials.</p>
<p>Also, note that the <code>.gitignore</code> file created by NextJS doesn't ignore the <code>.env</code> file. It is configured to ignore the <code>.env.local</code> file. But Prisma requires <code>.env</code>, so we will replace the <code>.gitignore</code> file content with the following:</p>
<pre><code class="lang-python"><span class="hljs-comment"># See https://help.github.com/articles/ignoring-files/ for more about ignoring files.</span>

<span class="hljs-comment"># dependencies</span>
/node_modules
/.pnp
.pnp.js

<span class="hljs-comment"># testing</span>
/coverage

<span class="hljs-comment"># next.js</span>
/.next/
/out/

<span class="hljs-comment"># production</span>
/build

<span class="hljs-comment"># misc</span>
.DS_Store
*.pem

<span class="hljs-comment"># debug</span>
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

<span class="hljs-comment"># local env files</span>
.env
.env*.local

<span class="hljs-comment"># vercel</span>
.vercel

<span class="hljs-comment"># typescript</span>
*.tsbuildinfo
next-env.d.ts
</code></pre>
<p>Ideally, Prisma manages schema migration using the <code>prisma migrate</code> command. But as PlanetScale has its schema migration mechanism inbuilt, we will use that. Use the following command to push migration to our current main branch.</p>
<p>Note, our main branch is not yet promoted as a production branch.</p>
<pre><code class="lang-shell">npx prisma db push
</code></pre>
<p>Now let's generate the Prisma client using the following command:</p>
<pre><code class="lang-shell">npx prisma generate
</code></pre>
<p>Go to the PlanetScale Dashboard, and there you'll find two tables created:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-2.59.50-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Two Tables Created</em></p>
<p>Click on these tables, and you'll be redirected to the following page:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-3.00.39-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Database Schema</em></p>
<h2 id="heading-how-to-implement-mock-data-category-products-api-and-all-category-single-category-ui">How to Implement Mock Data, Category-Products API, and All Category-Single Category UI.</h2>
<p>In this section, we'll implement the following functionality:</p>
<ol>
<li><p>Create mock data</p>
</li>
<li><p>Create a Category and Product API.</p>
</li>
<li><p>Create an All Category and Single Category UI.</p>
</li>
</ol>
<p>You can find the <strong>eCommerce website code</strong> implemented in this section at this <a target="_blank" href="https://github.com/Sharvin26/Ecommerce-Website-ReactJS-TailwindCSS-PlanetScale-Stripe/tree/18bfb1152cfdeb14ba1a554d88d2b766a319d66a">commit</a>.</p>
<h3 id="heading-how-to-create-the-mock-data">How to Create the Mock Data:</h3>
<p>Under the <code>prisma</code> directory, create a file named <code>seed.ts</code> and copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> {
    randBetweenDate,
    randNumber,
    randProduct,
    randProductAdjective,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@ngneat/falso"</span>;
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@prisma/client"</span>;

<span class="hljs-keyword">const</span> primsa = <span class="hljs-keyword">new</span> PrismaClient();

<span class="hljs-keyword">const</span> main = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> primsa.category.deleteMany();
        <span class="hljs-keyword">await</span> primsa.product.deleteMany();
        <span class="hljs-keyword">const</span> fakeProducts = randProduct({
            length: <span class="hljs-number">1000</span>,
        });
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> index = <span class="hljs-number">0</span>; index &lt; fakeProducts.length; index++) {
            <span class="hljs-keyword">const</span> product = fakeProducts[index];
            <span class="hljs-keyword">const</span> productAdjective = randProductAdjective();
            <span class="hljs-keyword">await</span> primsa.product.upsert({
                where: {
                    title: <span class="hljs-string">`<span class="hljs-subst">${productAdjective}</span> <span class="hljs-subst">${product.title}</span>`</span>,
                },
                create: {
                    title: <span class="hljs-string">`<span class="hljs-subst">${productAdjective}</span> <span class="hljs-subst">${product.title}</span>`</span>,
                    description: product.description,
                    price: product.price,
                    image: <span class="hljs-string">`<span class="hljs-subst">${product.image}</span>/tech`</span>,
                    quantity: randNumber({ min: <span class="hljs-number">10</span>, max: <span class="hljs-number">100</span> }),
                    category: {
                        connectOrCreate: {
                            where: {
                                name: product.category,
                            },
                            create: {
                                name: product.category,
                                createdAt: randBetweenDate({
                                    <span class="hljs-keyword">from</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-string">"10/06/2020"</span>),
                                    to: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
                                }),
                            },
                        },
                    },
                    createdAt: randBetweenDate({
                        <span class="hljs-keyword">from</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-string">"10/07/2020"</span>),
                        to: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
                    }),
                },
                update: {},
            });
        }
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">throw</span> error;
    }
};

main().catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">"Error While generating Seed: \n"</span>, err);
});
</code></pre>
<p>Here we are creating 1000 fake products and adding them to the database.</p>
<p>We are following these steps to add the products:</p>
<ol>
<li><p>Delete all the categories using the <code>deleteMany()</code> function.</p>
</li>
<li><p>Delete all the product using the <code>deleteMany()</code> function.</p>
</li>
<li><p>The above are optional steps, but it's always a good idea to rerun the seed script with a clean table.</p>
</li>
<li><p>As the <code>title</code> attribute from the <code>product</code> table has unique property associated with it we bind it with the <code>randProductAdjective</code> function output to make repetitions less likely.</p>
</li>
<li><p>But still, there is a probability that the title property created by the <code>falso</code> gets repeated. So we use the upsert method from <code>@prisma/client</code>.</p>
</li>
<li><p>We are also creating/associating the category when we create a product.</p>
</li>
</ol>
<p>Now go to <code>package.json</code> and update the following code below <code>scripts</code>:</p>
<pre><code class="lang-shell">"prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\" prisma/seed.ts"
},
</code></pre>
<p>We will use the <code>ts-node</code> package to run our seed script command. The seed script is written in TypeScript while <code>ts-node</code> converts TypeScript code to JavaScript.</p>
<p>Use the following command to install the package:</p>
<pre><code class="lang-shell">npm i --save-dev ts-node
</code></pre>
<p>As the <code>ts-node</code> will convert the code to JavaScript, we can execute the following command to seed the tables with mock data:</p>
<pre><code class="lang-shell">npx prisma db seed
</code></pre>
<p>You'll get the following output that will show it has started running. It will take some time to seed the tables with mock data.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-3.18.56-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Once the seed command is successful, you'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-3.20.09-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The benefit of Prisma is that it also has a studio, which can be used to view the database in a local development environment. Use the following command to run this studio:</p>
<pre><code class="lang-shell">npx prisma studio
</code></pre>
<p>Open <a target="_blank" href="http://localhost:5555">http://localhost:5555</a>, on your browser, and you'll get the following screen with all the tables:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-3.22.23-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Prisma Studio</em></p>
<p>The number of Products and Categories may vary on your side or be similar, as this is random data.</p>
<h3 id="heading-how-to-create-the-category-and-product-apis">How to Create the Category and Product APIs:</h3>
<p>Under the <code>src/pages/api</code> category you'll find a file named <code>hello.ts</code>. Remove this file and create two directories named <code>categories</code> and <code>products</code>.</p>
<p>Inside those categories, create a file named <code>index.ts</code> and copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> nc <span class="hljs-keyword">from</span> <span class="hljs-string">"next-connect"</span>;
<span class="hljs-keyword">import</span> { prisma } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../lib/prisma"</span>;
<span class="hljs-keyword">import</span> { TApiAllCategoriesResp, TApiErrorResp } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../types"</span>;

<span class="hljs-keyword">const</span> getCategories = <span class="hljs-keyword">async</span> (
    _req: NextApiRequest,
    res: NextApiResponse&lt;TApiAllCategoriesResp | TApiErrorResp&gt;
) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> categories = <span class="hljs-keyword">await</span> prisma.category.findMany({
            select: {
                id: <span class="hljs-literal">true</span>,
                name: <span class="hljs-literal">true</span>,
                products: {
                    orderBy: {
                        createdAt: <span class="hljs-string">"desc"</span>,
                    },
                    take: <span class="hljs-number">8</span>,
                    select: {
                        title: <span class="hljs-literal">true</span>,
                        description: <span class="hljs-literal">true</span>,
                        image: <span class="hljs-literal">true</span>,
                        price: <span class="hljs-literal">true</span>,
                    },
                },
            },
            orderBy: {
                createdAt: <span class="hljs-string">"desc"</span>,
            },
        });
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ categories });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">500</span>).json({
            message: <span class="hljs-string">"Something went wrong!! Please try again after sometime"</span>,
        });
    }
};

<span class="hljs-keyword">const</span> handler = nc({ attachParams: <span class="hljs-literal">true</span> }).get(getCategories);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> handler;
</code></pre>
<p>In the above snippet, we are doing the following:</p>
<ol>
<li><p>When we create a file under the <code>pages&gt;api</code> directory, NextJS treats it as a Serverless API. So by creating a file named <code>categories/index.ts</code> we are informing Next that it needs to convert this to the <code>/api/categories</code> API.</p>
</li>
<li><p>Using the <code>next-connect</code> library we are making sure that only the <code>get</code> operation is allowed for the <code>getCategories</code> function.</p>
</li>
<li><p>Under this function, we are querying the database with an order as <code>desc</code> for the <code>createdAt</code> property and we only take the latest eight product rows for each category row. We also select a specific property from the product and category that are required by the front end.</p>
</li>
</ol>
<p>We don't query all the products for each category in this API, because it will slow down our response time.</p>
<p>You'll find that we have imported <code>prisma</code> and <code>types</code> files. Let's create two directories under <code>src</code> named <code>lib</code> and <code>types</code>.</p>
<p>Under the <code>lib</code> directory, create a file named <code>prisma.ts</code>, and under the types directory create a file named <code>index.ts</code>.</p>
<p>Let's create our global Prisma constant under <code>prisma.ts</code>. Copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@prisma/client"</span>;

<span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
    <span class="hljs-keyword">var</span> prisma: PrismaClient;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> prisma =
    <span class="hljs-built_in">global</span>.prisma ||
    <span class="hljs-keyword">new</span> PrismaClient({
        log: [],
    });

<span class="hljs-keyword">if</span> (process.env.NODE_ENV !== <span class="hljs-string">"production"</span>) <span class="hljs-built_in">global</span>.prisma = prisma;
</code></pre>
<p>Here we are creating a global prisma variable which we can use across the project.</p>
<p>Let's add the types that we will use application-wide under <code>src/types/index.ts</code>.</p>
<p>Copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> TApiAllCategoriesResp = {
    categories: {
        id: <span class="hljs-built_in">string</span>;
        name: <span class="hljs-built_in">string</span>;
        products: {
            title: <span class="hljs-built_in">string</span>;
            description: <span class="hljs-built_in">string</span>;
            image: <span class="hljs-built_in">string</span>;
            price: <span class="hljs-built_in">string</span>;
        }[];
    }[];
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> TApiSingleCategoryWithProductResp = {
    category: {
        id: <span class="hljs-built_in">string</span>;
        name: <span class="hljs-built_in">string</span>;
        products: {
            id: <span class="hljs-built_in">string</span>;
            title: <span class="hljs-built_in">string</span>;
            description: <span class="hljs-built_in">string</span>;
            image: <span class="hljs-built_in">string</span>;
            price: <span class="hljs-built_in">string</span>;
            quantity: <span class="hljs-built_in">number</span>;
        }[];
        hasMore: <span class="hljs-built_in">boolean</span>;
    };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> TApiSingleProductResp = {
    product: {
        title: <span class="hljs-built_in">string</span>;
        description: <span class="hljs-built_in">string</span>;
        price: <span class="hljs-built_in">string</span>;
        quantity: <span class="hljs-built_in">number</span>;
        image: <span class="hljs-built_in">string</span>;
    };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> TApiErrorResp = {
    message: <span class="hljs-built_in">string</span>;
};
</code></pre>
<p>Here we are creating four types which will be used across the project.</p>
<p>I'll be using Postman to test this API. Postman is a utility for developing APIs. You can call the APIs, and Postman will show the response based on how you structure it.</p>
<p>Just update the URL in Postman to:</p>
<pre><code class="lang-shell">http://localhost:3000/api/categories
</code></pre>
<p>And you'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-3.58.53-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>All Categories Resp</em></p>
<p>Now let's create an API to get a single category's information with its products.</p>
<p>Under the <code>src/pages/api/categories</code> directory create a file named <code>[id].ts</code> and copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> nc <span class="hljs-keyword">from</span> <span class="hljs-string">"next-connect"</span>;
<span class="hljs-keyword">import</span> { prisma } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../lib/prisma"</span>;
<span class="hljs-keyword">import</span> {
    TApiErrorResp,
    TApiSingleCategoryWithProductResp
} <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../types"</span>;

<span class="hljs-keyword">const</span> getSingleCategory = <span class="hljs-keyword">async</span> (
    req: NextApiRequest,
    res: NextApiResponse&lt;TApiSingleCategoryWithProductResp | TApiErrorResp&gt;
) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> id = req.query.id <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
        <span class="hljs-keyword">const</span> cursorId = req.query.cursorId;
        <span class="hljs-keyword">if</span> (cursorId) {
            <span class="hljs-keyword">const</span> categoriesData = <span class="hljs-keyword">await</span> prisma.category.findUnique({
                where: {
                    id,
                },
                select: {
                    id: <span class="hljs-literal">true</span>,
                    name: <span class="hljs-literal">true</span>,
                    products: {
                        orderBy: {
                            createdAt: <span class="hljs-string">"desc"</span>,
                        },
                        take: <span class="hljs-number">12</span>,
                        skip: <span class="hljs-number">1</span>,
                        cursor: {
                            id: cursorId <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>,
                        },
                        select: {
                            id: <span class="hljs-literal">true</span>,
                            title: <span class="hljs-literal">true</span>,
                            description: <span class="hljs-literal">true</span>,
                            image: <span class="hljs-literal">true</span>,
                            price: <span class="hljs-literal">true</span>,
                            quantity: <span class="hljs-literal">true</span>,
                        },
                    },
                },
            });

            <span class="hljs-keyword">if</span> (!categoriesData) {
                <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">404</span>).json({ message: <span class="hljs-string">`Category not found`</span> });
            }

            <span class="hljs-keyword">let</span> hasMore = <span class="hljs-literal">true</span>;
            <span class="hljs-keyword">if</span> (categoriesData.products.length === <span class="hljs-number">0</span>) {
                hasMore = <span class="hljs-literal">false</span>;
            }

            <span class="hljs-keyword">return</span> res
                .status(<span class="hljs-number">200</span>)
                .json({ category: { ...categoriesData, hasMore } });
        }

        <span class="hljs-keyword">const</span> categoriesData = <span class="hljs-keyword">await</span> prisma.category.findUnique({
            where: {
                id,
            },
            select: {
                id: <span class="hljs-literal">true</span>,
                name: <span class="hljs-literal">true</span>,
                products: {
                    orderBy: {
                        createdAt: <span class="hljs-string">"desc"</span>,
                    },
                    take: <span class="hljs-number">12</span>,
                    select: {
                        id: <span class="hljs-literal">true</span>,
                        title: <span class="hljs-literal">true</span>,
                        description: <span class="hljs-literal">true</span>,
                        image: <span class="hljs-literal">true</span>,
                        price: <span class="hljs-literal">true</span>,
                        quantity: <span class="hljs-literal">true</span>,
                    },
                },
            },
        });
        <span class="hljs-keyword">if</span> (!categoriesData) {
            <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">404</span>).json({ message: <span class="hljs-string">`Category not found`</span> });
        }

        <span class="hljs-keyword">let</span> hasMore = <span class="hljs-literal">true</span>;
        <span class="hljs-keyword">if</span> (categoriesData.products.length === <span class="hljs-number">0</span>) {
            hasMore = <span class="hljs-literal">false</span>;
        }

        <span class="hljs-keyword">return</span> res
            .status(<span class="hljs-number">200</span>)
            .json({ category: { ...categoriesData, hasMore } });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">500</span>).json({
            message: <span class="hljs-string">"Something went wrong!! Please try again after sometime"</span>,
        });
    }
};

<span class="hljs-keyword">const</span> handler = nc({ attachParams: <span class="hljs-literal">true</span> }).get(getSingleCategory);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> handler;
</code></pre>
<p>In the above snippet, we are doing the following:</p>
<ol>
<li><p>By creating a file named <code>[id].ts</code> under <code>src/pages/api/categories</code> we are telling NextJS to convert this to the <code>/api/categories/[id]</code> API.</p>
</li>
<li><p>The <code>[id]</code> is the category id from the category table.</p>
</li>
<li><p>Using the <code>next-connect</code> library we are making sure that only the <code>get</code> operation is allowed for the <code>getSingleCategory</code> function.</p>
</li>
<li><p>Under this function, we are querying the database with order as <code>desc</code> for the <code>createdAt</code> property and we only take the latest twelve product rows. We also select a specific property from the product that is required by the front end.</p>
</li>
</ol>
<p>In this API, you will find that we have implemented pagination also. It helps us get more products under one category.</p>
<p>There are two kinds of pagination. One is cursor based, and another is offset-based pagination.</p>
<p>So why did we choose cursor-based pagination instead of offset-based pagination?</p>
<p><a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/pagination">According to the Prisma docs</a>,</p>
<blockquote>
<p>"Offset pagination does not scale at a database level. For example, if you skip 200,00 records and take the first 10, the database still has to traverse the first 200,00 records before returning the 10 that you asked for - this negatively affects performance."</p>
</blockquote>
<p>For more information read this <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/pagination">helpful guide</a>.</p>
<p>Update the URL in Postman to:</p>
<pre><code class="lang-shell">http://localhost:3000/api/categories/cl91683hp006d0mvlxlg5u176?cursorId=cl91685ht00b00mvllxjwzkqk
</code></pre>
<p>Our URL consists of two ids and you'll need to add <code>cl91683hp006d0mvlxlg5u176</code> from the previous all-category response. This <code>cl91685ht00b00mvllxjwzkqk</code> id is just the cursor of the product and you can add this as the last one you want.</p>
<p>You'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.07.44-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Single Category Resp</em></p>
<p>Now let's create an API to get single product information.</p>
<p>Under the <code>src/pages/api/products</code> directory create a file named <code>[title].ts</code> and copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> nc <span class="hljs-keyword">from</span> <span class="hljs-string">"next-connect"</span>;
<span class="hljs-keyword">import</span> { prisma } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../lib/prisma"</span>;
<span class="hljs-keyword">import</span> { TApiErrorResp, TApiSingleProductResp } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../../types"</span>;

<span class="hljs-keyword">const</span> getSingleProduct = <span class="hljs-keyword">async</span> (
    req: NextApiRequest,
    res: NextApiResponse&lt;TApiSingleProductResp | TApiErrorResp&gt;
) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> title = req.query.title <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
        <span class="hljs-keyword">const</span> product = <span class="hljs-keyword">await</span> prisma.product.findUnique({
            where: {
                title,
            },
            select: {
                title: <span class="hljs-literal">true</span>,
                description: <span class="hljs-literal">true</span>,
                price: <span class="hljs-literal">true</span>,
                quantity: <span class="hljs-literal">true</span>,
                image: <span class="hljs-literal">true</span>,
            },
        });
        <span class="hljs-keyword">if</span> (!product) {
            <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">404</span>).json({ message: <span class="hljs-string">`Product not found`</span> });
        }
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ product });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">500</span>).json({
            message: <span class="hljs-string">"Something went wrong!! Please try again after sometime"</span>,
        });
    }
};

<span class="hljs-keyword">const</span> handler = nc({ attachParams: <span class="hljs-literal">true</span> }).get(getSingleProduct);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> handler;
</code></pre>
<p>In the above snippet, we are doing the following:</p>
<ol>
<li><p>By creating a file named <code>[title].ts</code> under <code>src/pages/api/products</code> we are informing NextJS to convert this to the <code>/api/products/[title]</code> API.</p>
</li>
<li><p>The <code>[title]</code> is the product title from the product table.</p>
</li>
<li><p>Using the <code>next-connect</code> library we are making sure that only the <code>get</code> operation is allowed for the <code>getSingleProduct</code> function.</p>
</li>
<li><p>Under this function, we are querying the database using the <code>findUnique</code> query based on the title.</p>
</li>
</ol>
<p>Update the URL in Postman to:</p>
<pre><code class="lang-shell">http://localhost:3000/api/products/Practical Gorgeous Fresh Shoes
</code></pre>
<p>Here <code>Practical Gorgeous Fresh Shoes</code> is the title of the product we want to get. You can replace it with any product title from your database.</p>
<p>You'll get the following response:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.12.35-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Single Product Resp</em></p>
<h3 id="heading-how-to-create-the-all-category-and-single-category-uis">How to Create the All Category and Single Category UIs:</h3>
<p>Under <code>src/pages/_app.tsx</code>, replace the existing code with the following:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { QueryClientProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;
<span class="hljs-keyword">import</span> { ReactQueryDevtools } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query-devtools"</span>;
<span class="hljs-keyword">import</span> type { AppProps } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/app"</span>;
<span class="hljs-keyword">import</span> queryClient <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/query"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/globals.css"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyApp</span>(<span class="hljs-params">{ Component, pageProps }: AppProps</span>) </span>{
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">QueryClientProvider</span> <span class="hljs-attr">client</span>=<span class="hljs-string">{queryClient}</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ReactQueryDevtools</span> <span class="hljs-attr">initialIsOpen</span>=<span class="hljs-string">{false}</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Component</span> {<span class="hljs-attr">...pageProps</span>} /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">QueryClientProvider</span>&gt;</span></span>
    );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> MyApp;
</code></pre>
<p>Here we are wrapping all our components with React QueryClient Provider. But we also need to pass in the Client Context.</p>
<p>Under the <code>src/lib</code> directory create a new file named <code>query.ts</code> and copy-paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { QueryClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;

<span class="hljs-keyword">const</span> queryClient = <span class="hljs-keyword">new</span> QueryClient();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> queryClient;
</code></pre>
<p>We are initiating a new <code>QueryClient</code> object and assigning it to the <code>queryClient</code> variable and exporting it as default. The reason we do this is that in this way we get to keep the <code>queryClient</code> object as a global context.</p>
<p>Under <code>src/pages/index.tsx</code>, replace the existing code with the following:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;
<span class="hljs-keyword">import</span> type { NextPage } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">"next/head"</span>;
<span class="hljs-keyword">import</span> Navbar <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/Navbar"</span>;
<span class="hljs-keyword">import</span> ProductGrid <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/ProductGrid"</span>;
<span class="hljs-keyword">import</span> Skelton <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/Skelton"</span>;

<span class="hljs-keyword">const</span> Home: NextPage = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> getAllCategories = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> respJSON = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/categories"</span>);
            <span class="hljs-keyword">const</span> resp = <span class="hljs-keyword">await</span> respJSON.json();
            <span class="hljs-keyword">return</span> resp;
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">throw</span> error;
        }
    };

    <span class="hljs-keyword">const</span> { isLoading, data } = useQuery(
        [<span class="hljs-string">"AllCategoreiesWithProducts"</span>],
        getAllCategories
    );

    <span class="hljs-keyword">const</span> categories = data?.categories;

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Head</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>All Products<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"All Products"</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/favicon.ico"</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">Head</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container mx-auto"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
                {isLoading ? (
                    <span class="hljs-tag">&lt;<span class="hljs-name">Skelton</span> /&gt;</span>
                ) : (
                    <span class="hljs-tag">&lt;&gt;</span>
                        {categories &amp;&amp; categories?.length &gt; 0 &amp;&amp; (
                            <span class="hljs-tag">&lt;<span class="hljs-name">ProductGrid</span>
                                <span class="hljs-attr">showLink</span>=<span class="hljs-string">{true}</span>
                                <span class="hljs-attr">categories</span>=<span class="hljs-string">{categories}</span>
                            /&gt;</span>
                        )}
                    <span class="hljs-tag">&lt;/&gt;</span>
                )}
            <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
        &lt;/div&gt;
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home;
</code></pre>
<p>Let's understand our code.</p>
<p>Here we are fetching the data from the <code>/api/categories</code> endpoint that we wrote earlier. We are using <code>useQuery</code> to cache this data with the key <code>AllCategoreiesWithProducts</code>.</p>
<p>But there are three components that we haven't created yet. Let's create those and understand each one.</p>
<p>Under the <code>src</code> directory, create a <code>components</code> directory. Under the newly created <code>components</code> directory, create three files named <code>Navbar.tsx</code>, <code>ProductGrid.tsx</code> and <code>Skelton.tsx</code>.</p>
<p>Under <code>Navbar.tsx</code> copy-paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> NextLink <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;

<span class="hljs-keyword">const</span> Navbar = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"relative bg-white mx-6"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center justify-between pt-6 md:justify-start md:space-x-10"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex justify-start lg:w-0 lg:flex-1"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-2xl"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">NextLink</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"cursor-pointer"</span>&gt;</span>
                            Ecomm App
                        <span class="hljs-tag">&lt;/<span class="hljs-name">NextLink</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Navbar;
</code></pre>
<p>Here we have created an <code>h1</code> with the text as Ecomm App. We have wrapped this text around <code>NextLink</code> and set the location as <code>/</code>. So when user clicks on this, they will be redirected to the home page.</p>
<p>Under <code>ProductGrid.tsx</code> copy-paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> NextImage <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> NextLink <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;
<span class="hljs-keyword">import</span> { useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { AiOutlineRight } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/ai"</span>;
<span class="hljs-keyword">import</span> { useInView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-intersection-observer"</span>;
<span class="hljs-keyword">import</span> { TApiAllCategoriesResp } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types"</span>;

interface IProductGrid <span class="hljs-keyword">extends</span> TApiAllCategoriesResp {
    <span class="hljs-attr">showLink</span>: boolean;
    hasMore?: boolean;
    loadMoreFun?: <span class="hljs-built_in">Function</span>;
}

<span class="hljs-keyword">const</span> ProductGrid = <span class="hljs-function">(<span class="hljs-params">props: IProductGrid</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { categories, showLink, loadMoreFun, hasMore } = props;
    <span class="hljs-keyword">const</span> { ref, inView } = useInView();

    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (inView) {
            <span class="hljs-keyword">if</span> (loadMoreFun) loadMoreFun();
        }
    }, [inView, loadMoreFun]);

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-white"</span>&gt;</span>
            {categories.map((category) =&gt; (
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-12  p-6"</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{category.name}</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-row justify-between"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"inline-flex items-center rounded-md bg-sky-800 px-8 py-2 text-md font-medium text-white"</span>&gt;</span>
                            {category.name}
                        <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                        {showLink &amp;&amp; (
                            <span class="hljs-tag">&lt;<span class="hljs-name">NextLink</span> <span class="hljs-attr">href</span>=<span class="hljs-string">{</span>`/<span class="hljs-attr">category</span>/${<span class="hljs-attr">category.id</span>}`}&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-row gap-2 underline hover:cursor-pointer items-center"</span>&gt;</span>
                                    View More
                                    <span class="hljs-tag">&lt;<span class="hljs-name">AiOutlineRight</span> /&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">NextLink</span>&gt;</span>
                        )}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-6  grid grid-cols-1 gap-y-10 gap-x-6 xl:gap-x-8 sm:grid-cols-2 lg:grid-cols-4"</span>&gt;</span>
                        {category?.products.map((product) =&gt; (
                            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
                                <span class="hljs-attr">className</span>=<span class="hljs-string">"p-6 group rounded-lg border border-gray-200 bg-neutral-200"</span>
                                <span class="hljs-attr">key</span>=<span class="hljs-string">{product.title}</span>
                            &gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"min-h-80 w-full overflow-hidden rounded-md group-hover:opacity-75 lg:aspect-none lg:h-80"</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">NextImage</span>
                                        <span class="hljs-attr">priority</span>=<span class="hljs-string">{true}</span>
                                        <span class="hljs-attr">layout</span>=<span class="hljs-string">"responsive"</span>
                                        <span class="hljs-attr">width</span>=<span class="hljs-string">"25"</span>
                                        <span class="hljs-attr">height</span>=<span class="hljs-string">"25"</span>
                                        <span class="hljs-attr">src</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">product.image</span>}`}
                                        <span class="hljs-attr">alt</span>=<span class="hljs-string">{product.title}</span>
                                        <span class="hljs-attr">className</span>=<span class="hljs-string">"h-full w-full object-cover object-center lg:h-full lg:w-full"</span>
                                    /&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"relative mt-2"</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm font-medium text-gray-900"</span>&gt;</span>
                                        {product.title}
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-1 text-sm text-gray-500"</span>&gt;</span>
                                        {product.price}
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-6"</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">NextLink</span>
                                        <span class="hljs-attr">href</span>=<span class="hljs-string">{</span>`/<span class="hljs-attr">product</span>/${<span class="hljs-attr">product.title</span>}`}
                                    &gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"relative flex items-center justify-center rounded-md border border-transparent bg-sky-800 py-2 px-8 text-sm font-medium text-white hover:bg-sky-900 hover:cursor-pointer"</span>&gt;</span>
                                            View More Details
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">NextLink</span>&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                        ))}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    {!showLink &amp;&amp; (
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center justify-center mt-8"</span>&gt;</span>
                            {hasMore ? (
                                <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                                    <span class="hljs-attr">ref</span>=<span class="hljs-string">{ref}</span>
                                    <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>
                                    <span class="hljs-attr">className</span>=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900"</span>
                                &gt;</span>
                                    Loading...
                                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                            ) : (
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"border-l-4 border-yellow-400 bg-yellow-50 p-4 w-full"</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex"</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ml-3"</span>&gt;</span>
                                            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-yellow-700"</span>&gt;</span>
                                                You have viewed all the Products
                                                under this category.
                                            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                            )}
                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    )}
                    {showLink &amp;&amp; (
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full border-b border-gray-300 mt-24"</span> /&gt;</span>
                    )}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ProductGrid;
</code></pre>
<p>Here we have created a grid that will show 1 column for the base screen. For the sm screen it will show 2 columns and for the lg screen it will show 4 columns.</p>
<p>Under this, we have a single card which has a Title, Price, and View More Details button. The View More Details button redirects the user to a single product page which will create a bit later.</p>
<p>Apart from that, we are using the <code>useInView</code> hook from the <code>react-intersection-observer</code> library to find the user's cursor on the screen. This ref is attached to a <code>Loading...</code> button and once user is near it then we execute the <code>loadMoreFn</code> function.</p>
<p>It makes an API call to the server to get the next twelve rows from the last cursor.</p>
<p>Under <code>Skelton.tsx</code> copy-paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> Skelton = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-12 h-8 w-40 rounded-lg bg-gray-200"</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-6 grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-4 xl:gap-x-8"</span>&gt;</span>
                {Array(16)
                    .fill(0)
                    .map((_val, index) =&gt; (
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"rounded-2xl bg-black/5 p-4"</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"h-60 rounded-lg bg-gray-200"</span> /&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"space-y-4 mt-6 mb-4"</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"h-3 w-3/5 rounded-lg bg-gray-200"</span> /&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"h-3 w-4/5 rounded-lg bg-gray-200"</span> /&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    ))}
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/&gt;</span></span>
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Skelton;
</code></pre>
<p>We are using <code>placeimg</code> to get fake images for our product and we are using the Next Image component which requires that it be mentioned under <code>next.config.js</code>.</p>
<p>Replace the existing code in <code>next.config.js</code> with the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('next').NextConfig}</span> </span>*/</span>
<span class="hljs-keyword">const</span> nextConfig = {
    <span class="hljs-attr">reactStrictMode</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">swcMinify</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">images</span>: {
        <span class="hljs-attr">domains</span>: [<span class="hljs-string">"placeimg.com"</span>],
    },
};

<span class="hljs-built_in">module</span>.exports = nextConfig
</code></pre>
<p>We will need to restart our server. Use the following command to start your development server again:</p>
<pre><code class="lang-shell">npm run dev
</code></pre>
<p>Open <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a> and you'll see the following UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.31.29-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>All Products Page</em></p>
<p>Now let's create a single category page that the user can go to using a <code>View More</code> link.</p>
<p>Under <code>src/pages</code> create a directory named <code>category</code>. Under this directory create a file named <code>[id].tsx</code> and copy paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { useInfiniteQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">"next/head"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/router"</span>;
<span class="hljs-keyword">import</span> Navbar <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Navbar"</span>;
<span class="hljs-keyword">import</span> ProductGrid <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/ProductGrid"</span>;
<span class="hljs-keyword">import</span> Skelton <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Skelton"</span>;

<span class="hljs-keyword">const</span> SingleCategory = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">const</span> getSingleCategory = <span class="hljs-keyword">async</span> ({ pageParam = <span class="hljs-literal">null</span> }) =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">let</span> url = <span class="hljs-string">`/api/categories/<span class="hljs-subst">${router.query.id}</span>`</span>;
            <span class="hljs-keyword">if</span> (pageParam) {
                url += <span class="hljs-string">`?cursorId=<span class="hljs-subst">${pageParam}</span>`</span>;
            }
            <span class="hljs-keyword">const</span> respJSON = <span class="hljs-keyword">await</span> fetch(url);
            <span class="hljs-keyword">const</span> resp = <span class="hljs-keyword">await</span> respJSON.json();
            <span class="hljs-keyword">return</span> resp;
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">throw</span> error;
        }
    };

    <span class="hljs-keyword">const</span> { isLoading, data, fetchNextPage, isError } = useInfiniteQuery(
        [<span class="hljs-string">`singleCategory <span class="hljs-subst">${router.query.id <span class="hljs-keyword">as</span> string}</span>`</span>],
        getSingleCategory,
        {
            <span class="hljs-attr">enabled</span>: !!router.query.id,
            <span class="hljs-attr">getNextPageParam</span>: <span class="hljs-function">(<span class="hljs-params">lastPage</span>) =&gt;</span> {
                <span class="hljs-keyword">const</span> nextCursor =
                    lastPage?.category?.products[
                        lastPage?.category?.products?.length - <span class="hljs-number">1</span>
                    ]?.id;
                <span class="hljs-keyword">return</span> nextCursor;
            },
        }
    );

    <span class="hljs-keyword">const</span> allProductsWithCategory: any = {
        <span class="hljs-attr">name</span>: <span class="hljs-string">""</span>,
        <span class="hljs-attr">products</span>: [],
        <span class="hljs-attr">hasMore</span>: <span class="hljs-literal">true</span>,
    };

    data?.pages.map(<span class="hljs-function">(<span class="hljs-params">page</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (page?.category) {
            <span class="hljs-keyword">if</span> (page.category?.name) {
                allProductsWithCategory.name = page.category?.name;
            }
            <span class="hljs-keyword">if</span> (page.category?.products &amp;&amp; page.category?.products.length &gt; <span class="hljs-number">0</span>) {
                allProductsWithCategory.products.push(
                    ...page.category?.products
                );
            }
        }
        <span class="hljs-keyword">return</span> page?.category;
    });

    <span class="hljs-keyword">if</span> (data?.pages[data?.pages.length - <span class="hljs-number">1</span>]?.category?.products.length === <span class="hljs-number">0</span>) {
        allProductsWithCategory.hasMore = <span class="hljs-literal">false</span>;
    }

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Head</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>
                    {isLoading
                        ? "Loading..."
                        : `All ${allProductsWithCategory?.name} Product`}
                <span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">meta</span>
                    <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span>
                    <span class="hljs-attr">content</span>=<span class="hljs-string">"Generated by create next app"</span>
                /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/favicon.ico"</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">Head</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container mx-auto"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
                {isLoading ? (
                    <span class="hljs-tag">&lt;<span class="hljs-name">Skelton</span> /&gt;</span>
                ) : (
                    <span class="hljs-tag">&lt;&gt;</span>
                        {allProductsWithCategory &amp;&amp;
                            allProductsWithCategory.products.length &gt; 0 &amp;&amp; (
                                <span class="hljs-tag">&lt;<span class="hljs-name">ProductGrid</span>
                                    <span class="hljs-attr">hasMore</span>=<span class="hljs-string">{allProductsWithCategory.hasMore}</span>
                                    <span class="hljs-attr">showLink</span>=<span class="hljs-string">{false}</span>
                                    <span class="hljs-attr">categories</span>=<span class="hljs-string">{[allProductsWithCategory]}</span>
                                    <span class="hljs-attr">loadMoreFun</span>=<span class="hljs-string">{fetchNextPage}</span>
                                /&gt;</span>
                            )}
                    <span class="hljs-tag">&lt;/&gt;</span>
                )}
            <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
        &lt;/div&gt;
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SingleCategory;
</code></pre>
<p>Here we are calling the <code>/api/categories/[id]</code> API to get the latest twelve products for that category id.</p>
<p>We are using the <code>useInfiniteQuery</code> hook from <code>react query</code> to fetch the data. This hook is useful for cursor-based pagination. We will be using the <code>ProductGrid</code> component that we created earlier.</p>
<p>Open <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a>, click on the View More link for any of the category, and you'll see the following UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.38.25-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Single Category Page</em></p>
<p>The difference between the previous UI and the current is that we now don't have the View More Link in the top right corner. Also, when you scroll below, you'll get more products for that category.</p>
<p>Once we scroll through all the products in that category we will see the following warning alert:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.39.44-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-implement-single-product-ui-and-stripe-checkout">How to Implement Single Product UI and Stripe Checkout.</h2>
<p>In this section, we'll implement the following functionality:</p>
<ol>
<li><p>Create Single Product UI</p>
</li>
<li><p>Create Stripe Checkout</p>
</li>
</ol>
<p>You can find the <strong>eCommerce</strong> website <strong>code</strong> implemented in this section at this <a target="_blank" href="https://github.com/Sharvin26/Ecommerce-Website-ReactJS-TailwindCSS-PlanetScale-Stripe/tree/e4b5426423358479a5bbe91ba17b3febacd5e4a3">commit</a>.</p>
<h3 id="heading-how-to-create-the-single-product-ui">How to Create the Single Product UI:</h3>
<p>Under the <code>src/pages</code> directory create a directory named <code>product</code>.</p>
<p>Under this directory create a file named <code>[title].tsx</code> and copy-paste the following code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { loadStripe, Stripe } <span class="hljs-keyword">from</span> <span class="hljs-string">"@stripe/stripe-js"</span>;
<span class="hljs-keyword">import</span> { useMutation, useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">"next/head"</span>;
<span class="hljs-keyword">import</span> NextImage <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/router"</span>;
<span class="hljs-keyword">import</span> Navbar <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Navbar"</span>;
<span class="hljs-keyword">import</span> Skelton <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Skelton"</span>;

<span class="hljs-keyword">const</span> stripePromiseclientSide = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

<span class="hljs-keyword">const</span> SingleProduct = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-keyword">const</span> getSingleProduct = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> title = router?.query?.title;

            <span class="hljs-keyword">const</span> respJSON = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/products/<span class="hljs-subst">${title}</span>`</span>);
            <span class="hljs-keyword">const</span> resp = <span class="hljs-keyword">await</span> respJSON.json();
            <span class="hljs-keyword">return</span> resp;
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">throw</span> error;
        }
    };

    <span class="hljs-keyword">const</span> { mutate, <span class="hljs-attr">isLoading</span>: mutationIsLoading } = useMutation(
        <span class="hljs-keyword">async</span> (body: any) =&gt; {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">const</span> respJSON = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/create-checkout-session"</span>, {
                    <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
                    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(body),
                });
                <span class="hljs-keyword">const</span> resp = <span class="hljs-keyword">await</span> respJSON.json();
                <span class="hljs-keyword">const</span> stripe = (<span class="hljs-keyword">await</span> stripePromiseclientSide) <span class="hljs-keyword">as</span> Stripe;
                <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> stripe.redirectToCheckout({
                    <span class="hljs-attr">sessionId</span>: resp.id,
                });
                <span class="hljs-keyword">return</span> result;
            } <span class="hljs-keyword">catch</span> (error) {
                <span class="hljs-keyword">throw</span> error;
            }
        }
    );

    <span class="hljs-keyword">const</span> { data, isLoading } = useQuery(
        [<span class="hljs-string">`singleProduct, <span class="hljs-subst">${router?.query?.title}</span>`</span>],
        getSingleProduct,
        {
            <span class="hljs-attr">enabled</span>: !!router?.query?.title,
        }
    );

    <span class="hljs-keyword">const</span> product = data?.product;

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Head</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>{isLoading ? "Loading..." : `${product?.title}`}<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">meta</span>
                    <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span>
                    <span class="hljs-attr">content</span>=<span class="hljs-string">"Generated by create next app"</span>
                /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/favicon.ico"</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">Head</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container mx-6 md:mx-auto"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
                {isLoading ? (
                    <span class="hljs-tag">&lt;<span class="hljs-name">Skelton</span> /&gt;</span>
                ) : (
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-white"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"pt-6 pb-16 sm:pb-24"</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mx-auto mt-8"</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col md:flex-row gap-x-8"</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"min-h-80 w-full overflow-hidden rounded-md group-hover:opacity-75 lg:aspect-none lg:h-80"</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">NextImage</span>
                                            <span class="hljs-attr">layout</span>=<span class="hljs-string">"responsive"</span>
                                            <span class="hljs-attr">width</span>=<span class="hljs-string">"25"</span>
                                            <span class="hljs-attr">height</span>=<span class="hljs-string">"25"</span>
                                            <span class="hljs-attr">src</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">product.image</span>}`}
                                            <span class="hljs-attr">alt</span>=<span class="hljs-string">{product.title}</span>
                                            <span class="hljs-attr">className</span>=<span class="hljs-string">"h-full w-full object-cover object-center lg:h-full lg:w-full"</span>
                                        /&gt;</span>
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"lg:col-span-5 lg:col-start-8 mt-8 md:mt-0"</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-xl font-medium text-gray-900 "</span>&gt;</span>
                                            {product.title}
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-xl font-light text-gray-700 mt-4"</span>&gt;</span>
                                            {product.description}
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-xl font-normal text-gray-500 mt-4"</span>&gt;</span>
                                            USD {product.price}
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                                        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                                            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span>
                                                mutate({
                                                    title: product.title,
                                                    image: product.image,
                                                    description:
                                                        product.description,
                                                    price: product.price,
                                                })
                                            }
                                            disabled={mutationIsLoading}
                                            type="button"
                                            className="inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900  mt-4"
                                        &gt;
                                            Buy Now
                                        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                )}
            <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SingleProduct;
</code></pre>
<p>Here we are calling <code>/api/products/title</code> API to get the latest product. We have also created Stripe Interface for creating a checkout method once a user clicks the Buy Now button.</p>
<p>Once the user clicks the Buy Now button, we make an API call to <code>/api/create-checkout-session</code> using the <code>useMutation</code> hook. On a successful response, we redirect the user to the Stripe default checkout page.</p>
<p>Open <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a> and click on View More Details button for any product.</p>
<p>You'll see the following UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-5.54.40-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Single Product Page</em></p>
<p>You can also visit this page by clicking on the View More link and then clicking on the View More Details button for any product.</p>
<h3 id="heading-how-to-set-up-stripe-checkout">How to Set Up Stripe Checkout:</h3>
<p>To set up Stripe Checkout, we need to add a new file under the lib directory.</p>
<p>Create a new file named <code>stripe.ts</code> under the lib directory and copy paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> Stripe <span class="hljs-keyword">from</span> <span class="hljs-string">"stripe"</span>;

<span class="hljs-keyword">const</span> stripeServerSide = <span class="hljs-keyword">new</span> Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: <span class="hljs-string">"2022-08-01"</span>,
});

<span class="hljs-keyword">export</span> { stripeServerSide };
</code></pre>
<p>Here we are creating server-side instances of Stripe. Now under the <code>pages/api</code> directory, create a new file named <code>create-checkout-session.ts</code> and copy-paste the following code:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> currency <span class="hljs-keyword">from</span> <span class="hljs-string">"currency.js"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> nc <span class="hljs-keyword">from</span> <span class="hljs-string">"next-connect"</span>;
<span class="hljs-keyword">import</span> { stripeServerSide } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/stripe"</span>;
<span class="hljs-keyword">import</span> { TApiErrorResp } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../types"</span>;

<span class="hljs-keyword">const</span> checkoutSession = <span class="hljs-keyword">async</span> (
    req: NextApiRequest,
    res: NextApiResponse&lt;<span class="hljs-built_in">any</span> | TApiErrorResp&gt;
) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> host = req.headers.origin;
        <span class="hljs-keyword">const</span> referer = req.headers.referer;
        <span class="hljs-keyword">const</span> body = <span class="hljs-built_in">JSON</span>.parse(req.body);
        <span class="hljs-keyword">const</span> formatedPrice = currency(body.price, {
            precision: <span class="hljs-number">2</span>,
            symbol: <span class="hljs-string">""</span>,
        }).multiply(<span class="hljs-number">100</span>);
        <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> stripeServerSide.checkout.sessions.create({
            mode: <span class="hljs-string">"payment"</span>,
            payment_method_types: [<span class="hljs-string">"card"</span>],
            line_items: [
                {
                    price_data: {
                        currency: <span class="hljs-string">"usd"</span>,
                        product_data: {
                            name: body?.title,
                            images: [body.image],
                            description: body?.description,
                        },
                        unit_amount_decimal: formatedPrice.toString(),
                    },
                    quantity: <span class="hljs-number">1</span>,
                },
            ],
            success_url: <span class="hljs-string">`<span class="hljs-subst">${host}</span>/thank-you`</span>,
            cancel_url: <span class="hljs-string">`<span class="hljs-subst">${referer}</span>?status=cancel`</span>,
        });
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ id: session.id });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">500</span>).json({
            message: <span class="hljs-string">"Something went wrong!! Please try again after sometime"</span>,
        });
    }
};

<span class="hljs-keyword">const</span> handler = nc({ attachParams: <span class="hljs-literal">true</span> }).post(checkoutSession);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> handler;
</code></pre>
<p>In the snippet above we are doing the following:</p>
<ol>
<li><p>Formatting the price with precision as two and multiplying it by 100 as Stripe expects the unit_amount in cents by default.</p>
</li>
<li><p>We create the session and pass the id as the response.</p>
</li>
</ol>
<p>Now we need to create another page named <code>thank-you.tsx</code> under the <code>src/pages</code> directory. Once the product purchase is successful, Stripe Checkout will redirect to this page.</p>
<p>Copy-paste the following code under this file:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">"next/head"</span>;
<span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/router"</span>;
<span class="hljs-keyword">import</span> { HiCheckCircle } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-icons/hi"</span>;
<span class="hljs-keyword">import</span> Navbar <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/Navbar"</span>;

<span class="hljs-keyword">const</span> ThankYou = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> router = useRouter();
    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">Head</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Thank You<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"All Products"</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/favicon.ico"</span> /&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">Head</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container mx-auto"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"rounded-md bg-green-50 p-4 mt-8"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex-shrink-0"</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">HiCheckCircle</span>
                                <span class="hljs-attr">className</span>=<span class="hljs-string">"h-5 w-5 text-green-400"</span>
                                <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>
                            /&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"ml-3"</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm font-medium text-green-800"</span>&gt;</span>
                                Order Placed
                            <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-2 text-sm text-green-700"</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
                                    Thank you for your Order. We have placed the
                                    order and your email will recieve further
                                    details.
                                <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> router.push("/")}
                                type="button"
                                className="inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900 mt-4"
                            &gt;
                                Continue Shopping
                            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ThankYou;
</code></pre>
<p>Open <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a> and click on the View More Details button of any product.</p>
<p>Click on the Buy Now button and you'll be redirected to the following page:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-6.09.52-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Add all the details for the test card. You can use any card from this <a target="_blank" href="https://stripe.com/docs/testing?numbers-or-method-or-token=card-numbers#cards">link</a>. Stripe provides various test cards which work only during Test Mode. Once you click on Pay and payment processing happens, Stripe will redirect you to the success page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-09-at-6.14.51-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Thank You Page</em></p>
<h2 id="heading-how-to-deploy-the-website-to-production">How to Deploy the Website to Production</h2>
<p>In this section, we'll implement the following functionality:</p>
<ol>
<li><p>Promote our PlanetScale branch to Main.</p>
</li>
<li><p>Deploy the app on Vercel.</p>
</li>
</ol>
<h3 id="heading-how-to-promote-the-planetscale-branch-to-main">How to Promote the PlanetScale Branch to Main:</h3>
<p>To promote the branch to main, we can do it either via the terminal or dashboard. I'll use the dashboard for this tutorial.</p>
<p>Go to your project on PlanetScale and you'll find the following message on the dashboard:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.19.14-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>PlanetScale Database Promotion</em></p>
<p>Let's click on the Promote a branch to production button and you'll get a confirmation model. Click on the Promote branch button. Once done you'll get a toast with a success message.</p>
<h3 id="heading-how-to-deploy-to-vercel">How to Deploy to Vercel:</h3>
<p>If you don't have an account on Vercel, you can create one <a target="_blank" href="https://vercel.com/signup">here</a>.</p>
<p>You can create a project on GitHub and push it to the Main branch. If you don't know how, you can check out <a target="_blank" href="https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-to-github#adding-a-local-repository-to-github-using-git">this tutorial</a>.</p>
<p>Once the project is pushed on GitHub, go to Vercel and create an Add New button and select Project from the drop down.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.26.04-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Add New Project Vercel</em></p>
<p>You'll get the following the UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.26.39-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Select Git Provider Vercel</em></p>
<p>As we have pushed the code on GitHub, let's click on the Continue with GitHub button. You'll get the following UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.28.03-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Select Git Repository Vercel</em></p>
<p>Click on Import and you'll get the following UI:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.29.39-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Configure Project Vercel</em></p>
<p>Click on the Environment Variables and add these three there:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.31.02-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, and DATABASE_URL</em></p>
<p>Once done click the Deploy button. You'll get the following UI once the deployment starts:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.31.52-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Deploying Vercel</em></p>
<p>Once deployed, Vercel will give you a Unique URL.</p>
<p>Visit this URL and you'll find its failing. Let's go to the deployment &gt; functions and you'll see the following error:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/Screenshot-2022-10-10-at-4.38.08-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Prisma generate Fails</em></p>
<p>We need to update our build command in <code>package.json</code> as follows:</p>
<pre><code class="lang-shell">"build": "npx prisma generate &amp;&amp; next build",
</code></pre>
<p>Push the code again to the Git repository and you'll find that Vercel starts redeploying your project.</p>
<p>Once the deployment is done, you can visit your application URL and you'll find it shows all your products.</p>
<p>With this, we have created our production-ready eCommerce application. If you have built the website along with the tutorial, then a very big congratulations to you on this achievement.</p>
<h2 id="heading-thank-you-for-reading"><strong>Thank you for reading!</strong></h2>
<p>Feel free to connect with me on <a target="_blank" href="https://twitter.com/sharvinshah26">Twitter</a> and <a target="_blank" href="https://github.com/Sharvin26">Github</a>.</p>
<p>If you want any project to be developed or want to consult with me, you can DM me on my Twitter (<a target="_blank" href="https://twitter.com/sharvinshah26">@sharvinshah26</a> ).</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Excellent Stripe Integration with Node.js: 4 Best Practices and Examples ]]>
                </title>
                <description>
                    <![CDATA[ By Ben Sears Have you ever woken up in the middle of the night, worried that you are not using the Stripe npm module properly? Probably not, but this article will help put your troubled soul at ease anyway with some interactive Node.js examples that ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/stripe-and-node-js-4-best-practices-and-examples/</link>
                <guid isPermaLink="false">66d45de2c7632f8bfbf1e3ff</guid>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ stripe ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 28 Oct 2019 17:23:26 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/10/BLOG_003_Stripe-and-Node.js_-4-Best-Practices-and-Examples.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ben Sears</p>
<p>Have you ever woken up in the middle of the night, worried that you are not using the Stripe npm module properly? Probably not, but this article will help put your troubled soul at ease anyway with some interactive Node.js examples that explain how to build an excellent Stripe integration.</p>
<h2 id="heading-1-use-auto-pagination-to-avoid-bloated-code">1. Use auto-pagination to avoid bloated code</h2>
<p>Pagination is a necessary evil that saves us from loading too much data, but dealing with it in code can be a pain. Before <code>v6.11.0</code>, your Stripe code would look something like this to deal with pagination:</p>
<h4 id="heading-this-example-shows-the-old-way-of-handling-pagination-in-stripe">This example shows the old way of handling pagination in Stripe</h4>
<pre>//require Stripe's Node bindings
const stripe = require("stripe")("rk_test_72wdhn7pifTOWbrtrSNFxhsQ00NrdzPvaC")

//get first 100 invoices
let invoices = await stripe.invoices.list({limit: 100});
let numberProcessed = 0;

//loop through these invoices
for(let invoice of invoices.data){
    numberProcessed++;
}

//has_more indicates if we need to deal with pagination
while(invoices.has_more){

    //starting_after will be the the id of the last result
    invoices = await stripe.invoices.list({limit: 100, starting_after: invoices.data[invoices.data.length -1].id});

    //loop through the next 100
    for(let invoice of invoices.data){
        numberProcessed++;
    }
    console.log("Number processed so far: " + numberProcessed);
}
console.log("Total Number Processed: " + numberProcessed);
</pre>

<p>With the introduction of  auto-pagination in <code>v6.11.0</code>, we are now able to have a much more efficient way of paginating: </p>
<h4 id="heading-this-example-shows-how-to-auto-paginate-in-stripe">This example shows how to auto-paginate in Stripe</h4>
<pre>//require Stripe's Node bindings
const stripe = require("stripe")("rk_test_72wdhn7pifTOWbrtrSNFxhsQ00NrdzPvaC")

//get all invoices
const allInvoices = await stripe.invoices.list({limit: 100}).autoPagingToArray({limit: 10000});
console.log("Invoices - " + allInvoices.length);
</pre>

<blockquote>
<p>Note: You need to be running Node.js v10 or above for this. </p>
</blockquote>
<h2 id="heading-2-use-expand-to-reduce-the-number-of-api-calls">2. Use expand to reduce the number of API calls</h2>
<p>In Stripe, there are a lot of different objects. A lot of times, when dealing with one type of object, say a subscription; you want to get the product that subscription belongs. To get the product, you need to make an extra call to Stripe as shown here:</p>
<h4 id="heading-this-example-shows-how-to-get-the-product-from-a-subscription-in-stripe-without-using-expand">This example shows how to get the product from a subscription in Stripe without using expand</h4>
<pre>//require Stripe's Node bindings
const stripe = require("stripe")("rk_test_3U9s3aPLquPOczvc4FVRQKdo00AhMZlMIE")

const subscription = await stripe.subscriptions.retrieve("sub_G0zK9485afDl6O");
const product = await stripe.products.retrieve(subscription.plan.product);
console.log(product.name);
</pre>

<p>We can effectively avoid this by using the <a target="_blank" href="https://stripe.com/docs/api/expanding_objects">"expand" attribute in Stripe's API</a>:</p>
<h4 id="heading-this-example-shows-getting-the-product-by-using-expand">This example shows getting the product by using expand</h4>
<pre>//require Stripe's Node bindings
const stripe = require("stripe")("rk_test_3U9s3aPLquPOczvc4FVRQKdo00AhMZlMIE")

//expand the product inside the plan
const subscription = await stripe.subscriptions.retrieve("sub_G0zK9485afDl6O", {expand: "plan.product"});
console.log(subscription.plan.product.name);
</pre>

<p>Cutting down on API calls will improve your app's performance and reduce the risk of hitting Stripe's API limits.</p>
<h2 id="heading-3-configure-your-stripe-connection-for-a-more-stable-experience">3. Configure your Stripe connection for a more stable experience</h2>
<p>Most people with a simple Stripe integration will define a new Stripe connection on the fly without configuring it first like so:</p>
<p><code>const stripe = require("stripe")("STRIPE_SECRET_KEY");</code></p>
<p>When scaling your billing system, consider doing the following to improve your integration quality:</p>
<ul>
<li><strong>Lock your API version to avoid being affected by API changes</strong></li>
<li><strong>Set to Retry Automatically in case of network failure</strong></li>
<li><strong>Define your app information to help the Stripe team</strong></li>
</ul>
<h4 id="heading-heres-an-example-function-that-returns-a-configured-stripe-connection">Here's an example function that returns a configured Stripe connection</h4>
<pre>function createStripeConnection(stripe_api_key){
    const Stripe = require("stripe");
    const stripe = Stripe(stripe_api_key);
    stripe.setApiVersion('2019-03-14');//lock API version down to avoid code breaking
    stripe.setAppInfo({
        name: 'Servicebot',
        version: "1.1.3", //Optional
        url: 'https://servicebot.io' // Optional
    });
    stripe.setMaxNetworkRetries(3); //retry on network failure
    return stripe;
}

const stripe = createStripeConnection("rk_test_72wdhn7pifTOWbrtrSNFxhsQ00NrdzPvaC");
console.log(await stripe.invoices.list());
</pre>


<h2 id="heading-4-use-webhooks-to-process-events-that-occur-in-stripe">4. Use Webhooks to process events that occur in Stripe</h2>
<p>Webhooks play an essential role in most Stripe integrations. There are <a target="_blank" href="https://stripe.com/docs/api/events/types">a lot of different</a> events that happen, so which ones should you care about?</p>
<p>The most important webhook as a SaaS app to pay attention to is the <a target="_blank" href="https://stripe.com/docs/api/events/types#event_types-customer.subscription.deleted">customer.subscription.deleted</a> - when a subscription goes into state cancelled. You listen for this event in order to decide what to do with someone's account when they cancel, trial runs out, or their card fails. </p>
<p>Once you start listening to Stripe events, it is a good idea to secure your webhook receiver as not to be fed phony webhooks by a bad-actor. You do this by  utilizing Stripe's webhook si    gning functionality: </p>
<h3 id="heading-this-example-shows-how-to-validate-a-webhook-has-come-from-stripe">This example shows how to validate a webhook has come from Stripe</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// Set your secret key: remember to change this to your live secret key in production</span>
<span class="hljs-comment">// See your keys here: https://dashboard.stripe.com/account/apikeys</span>
<span class="hljs-keyword">const</span> stripe = <span class="hljs-built_in">require</span>(<span class="hljs-string">'stripe'</span>)(<span class="hljs-string">'sk_test_bkoS59kZFWBR3XZgkiHwozoX00lD4ttSs1'</span>);

<span class="hljs-comment">// Find your endpoint's secret in your Dashboard's webhook settings</span>
<span class="hljs-keyword">const</span> endpointSecret = <span class="hljs-string">'whsec_...'</span>;

<span class="hljs-comment">// This example uses Express to receive webhooks</span>
<span class="hljs-keyword">const</span> app = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>)();

<span class="hljs-comment">// Use body-parser to retrieve the raw body as a buffer</span>
<span class="hljs-keyword">const</span> bodyParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'body-parser'</span>);

<span class="hljs-comment">// Match the raw body to content type application/json</span>
app.post(<span class="hljs-string">'/webhook'</span>, bodyParser.raw({<span class="hljs-attr">type</span>: <span class="hljs-string">'application/json'</span>}), <span class="hljs-function">(<span class="hljs-params">request, response</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> sig = request.headers[<span class="hljs-string">'stripe-signature'</span>];

  <span class="hljs-keyword">let</span> event;

  <span class="hljs-keyword">try</span> {
    event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
  }
  <span class="hljs-keyword">catch</span> (err) {
    response.status(<span class="hljs-number">400</span>).send(<span class="hljs-string">`Webhook Error: <span class="hljs-subst">${err.message}</span>`</span>);
  }

  <span class="hljs-comment">// Handle the event</span>
  <span class="hljs-keyword">switch</span> (event.type) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'payment_intent.succeeded'</span>:
      <span class="hljs-keyword">const</span> paymentIntent = event.data.object;
      handlePaymentIntentSucceeded(paymentIntent);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'payment_method.attached'</span>:
      <span class="hljs-keyword">const</span> paymentMethod = event.data.object;
      handlePaymentMethodAttached(paymentMethod);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-comment">// ... handle other event types</span>
    <span class="hljs-keyword">default</span>:
      <span class="hljs-comment">// Unexpected event type</span>
      <span class="hljs-keyword">return</span> response.status(<span class="hljs-number">400</span>).end();
  }

  <span class="hljs-comment">// Return a response to acknowledge receipt of the event</span>
  response.json({<span class="hljs-attr">received</span>: <span class="hljs-literal">true</span>});
});

app.listen(<span class="hljs-number">8000</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Running on port 8000'</span>));
</code></pre>
<hr>
<h2 id="heading-avoid-the-effort-of-building-and-maintaining-a-complex-stripe-integration">Avoid the effort of building and maintaining a complex Stripe Integration</h2>
<p>Your billing code can get pretty complicated when it comes to having a fully-featured solution that includes coupons, free trials, metered billing, and more.</p>
<p>Building a user interface for your Stripe integration could take months to develop. <a target="_blank" href="https://servicebot.io">Servicebot</a> provides a drop-in UI for Stripe Billing. It takes less than an hour to set up and doesn’t require any development effort.</p>
<p><a href="https://servicebot.io"><img src="https://i.imgur.com/QJkpyHN.png" width="1600" height="753" alt="QJkpyHN" loading="lazy"></a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to implement 3DS2 with Stripe for SCA compliance under PSD2 in Europe ]]>
                </title>
                <description>
                    <![CDATA[ By Ben Sears What are PSD2, SCA, and 3DS? PSD2 The second Payment Services Directive (PSD2) is an EU directive announced in 2015. The goal of PSD2 is to protect people when they pay online, promote open banking, and make cross-border European payment... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/implement-3ds2-for-your-saas-using-stripe-billing-and-be-sca-compliant-for-pds2/</link>
                <guid isPermaLink="false">66d45de0d7a4e35e38434949</guid>
                
                    <category>
                        <![CDATA[ 3DS2 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ psd2 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 3ds ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SCA ]]>
                    </category>
                
                    <category>
                        <![CDATA[ stripe ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 18 Oct 2019 20:27:18 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/10/BLOG_002_Implement-3DS2-for-your-SaaS-using-Stripe-Billing-and-be-SCA-compliant-for-PDS2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ben Sears</p>
<h1 id="heading-what-are-psd2-sca-and-3ds"><strong>What are PSD2, SCA, and 3DS?</strong></h1>
<h2 id="heading-psd2"><strong>PSD2</strong></h2>
<p>The second Payment Services Directive (PSD2) is an EU directive announced in 2015. The goal of PSD2 is to protect people when they pay online, promote <a target="_blank" href="https://en.wikipedia.org/wiki/Open_banking">open banking</a>, and make cross-border European payment services safer. It went into effect September of 2019.</p>
<h2 id="heading-sca"><strong>SCA</strong></h2>
<p>Strong Customer Authentication (SCA) is a <a target="_blank" href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=uriserv:OJ.L_.2018.069.01.0023.01.ENG&amp;toc=OJ:L:2018:069:TOC">requirement of the PSD2</a> that ensures online payments are performed with multi-factor authentication to increase the security of online payments. Even though PSD2 was enacted in September of 2019, SCA has been delayed by 18 months to allow merchants and banks more time to implement solutions.</p>
<h2 id="heading-3ds2"><strong>3DS2</strong></h2>
<p>3-D Secure 2.0 (3DS2) is the second iteration of the 3DS, used to power brand-name systems such as <a target="_blank" href="https://usa.visa.com/visa-everywhere/security.html">Visa Secure</a>, <a target="_blank" href="https://www.mastercard.us/en-us/merchants/safety-security/identity-check.html">Mastercard Identity Check</a>, and <a target="_blank" href="https://network.americanexpress.com/globalnetwork/safekey/us/en/">American Express SafeKey</a>. It was designed to reduce fraud and provide added security to online payments and supported by many major banks.</p>
<p>3DS2 is considered an SCA compliant solution. If your business implements 3DS2, you will no longer be in danger of having your charges declined by banks.</p>
<h1 id="heading-does-sca-affect-your-saas-business"><strong>Does SCA affect your SaaS business?</strong></h1>
<p><img src="https://blog.servicebot.io/content/images/2019/10/image-21.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>SCA is considered in-effect on all e-commerce payments when both:</p>
<ul>
<li>The business is in the EU</li>
<li>The customer's bank is in the EU</li>
</ul>
<p>If SCA applies to you and you do not authenticate your customer's transactions you risk <strong>having charges declined by banks</strong>. </p>
<p>There are exemptions for several types of transactions defined in <a target="_blank" href="https://eba.europa.eu/documents/10180/1761863/Final+draft+RTS+on+SCA+and+CSC+under+PSD2+%28EBA-RTS-2017-02%29.pdf">Articles 12-18 of the PSD2</a>. As a SaaS company, the most critical exception to note is <strong>Article 13.</strong> This article states that recurring transactions do not need to be subject to SCA. What this means is that you only need to have an SCA implementation to handle the initial creation of a subscription and not the subsequent recurring charges.</p>
<p>If you are interested in reading a breakdown of the other exemptions and how they may apply to you, <a target="_blank" href="https://stripe.com/guides/strong-customer-authentication#exemptions-to-strong-customer-authentication">Stripe goes into depth on each here</a>.</p>
<h1 id="heading-should-you-be-sca-ready-even-if-you-arent-in-europe"><strong>Should you be SCA-ready even if you aren't in Europe?</strong></h1>
<p>There are benefits to implementing a solution such as 3DS2, even if you aren't affected by PSD2 or SCA. By implementing 3DS2, you will handle customer information in a much more secure manner, as well as shifting liability from you to the card issuer, reducing the risk of chargebacks.</p>
<h1 id="heading-how-do-you-become-sca-compliant"><strong>How do you become SCA compliant?</strong></h1>
<p>Being SCA compliant as a SaaS means that all online payments are authorized using two of the three elements,</p>
<p><img src="https://blog.servicebot.io/content/images/2019/10/image-19.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>As I mentioned before, 3DS2 is an SCA-compliant solution. Drop-in solutions such as <a target="_blank" href="https://servicebot.io">Servicebot</a>, PayPal, and <a target="_blank" href="https://stripe.com/payments/checkout">Stripe Checkout</a> already use 3DS2 and are therefore SCA-compliant. If you are using a custom-built solution using something like Stripe Billing or Braintree to manage your subscriptions, you will need to develop a 3DS2 implementation.</p>
<h1 id="heading-how-do-you-implement-3ds2-using-stripe-billing"><strong>How do you implement 3DS2 using Stripe Billing?</strong></h1>
<p><img src="https://blog.servicebot.io/content/images/2019/10/image-22.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Stripe has created two new objects as part of offering an SCA-compliant solution, PaymentIntent and SetupIntent, to facilitate using 3DS2. A PaymentIntent represents the intent to charge someone and is used as part of a payment authentication flow. SetupIntents are similar to PaymentIntents, but they represent the intent to charge someone's card eventually. You will use SetupIntents if your SaaS has a free trial, or offers a free tier, essentially anywhere a credit card will be charged at a later date.</p>
<h2 id="heading-using-paymentintents"><strong>Using PaymentIntents</strong></h2>
<p>If you are using Stripe Billing to create subscriptions, you are already using PaymentIntents by default. They are created and attached to each invoice for every new subscription. If you want to know if a new subscription requires SCA, you can check the status of the <code>payment_intent</code> on the <code>latest_invoice</code> of the subscription. The object will contain a <code>status</code> of <code>requires_action</code> - Run the following NodeJS code to see it in action.</p>
<h2 id="heading-this-code-creates-a-subscription-that-requires-sca">This code creates a subscription that requires SCA</h2>
<pre>const STRIPE_TEST_SECRET_KEY = "rk_test_3U9s3aPLquPOczvc4FVRQKdo00AhMZlMIE";
let stripe = require("stripe")(STRIPE_TEST_SECRET_KEY);
const sub = await stripe.subscriptions.create({ //creates a SCA-required subscription
    items: [{plan : "plan_FvnU01xoIPrg9l"}], //$300 per month plan without free trial
    customer: "cus_G0juGVZSLskx57",
    default_payment_method: "pm_1FUiR8CISNxwKLmI8uIQDdnv", //This PaymentMethod always requires SCA
    expand: ["latest_invoice.payment_intent"] //we expand the payload to show up the payment intent
});
const paymentIntent = sub.latest_invoice.payment_intent;
console.log(`Subscription Status: ${sub.status}`);
console.log(`PaymentIntent Status: ${paymentIntent.status}`)
console.log(paymentIntent.status === "requires_action" ? "SCA Required" : "No SCA Required");
console.log(sub);
</pre>

<p>Once you know you have a subscription that requires authentication, you can use the PaymentIntent's client_secret on the browser to start a 3DS2 Authentication process using Stripe.js</p>
<h2 id="heading-using-stripejs-handlecardpayment-with-the-paymentintent"><strong>Using Stripe.js handleCardPayment with the PaymentIntent</strong></h2>
<p>Stripe.js has a handy function called <a target="_blank" href="https://stripe.com/docs/stripe-js/reference#stripe-handle-card-payment">handleCardPayment</a>, which takes in a client secret from a payment intent and starts the 3DS2 process to authenticate the payment.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> stripe.handleCardPayment(<span class="hljs-string">'PAYMENTINTENT_SECRET'</span>);
</code></pre>
<p>You can see this in action here</p>
<div class="embed-wrapper">
        <iframe width="100%" height="350" src="https://codepen.io/bsears/embed/PooGOLg?editors=1111" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="CodePen embed" scrolling="no" allowtransparency="true" allowfullscreen="true" loading="lazy"></iframe></div>
<p>Once the customer authenticates, the subscription will move from an <code>incomplete</code> state to an <code>active</code> one, and the customer will be billed successfully.</p>
<h2 id="heading-setupintents"><strong>SetupIntents</strong></h2>
<p>As a SaaS business, you will mostly be interacting with SetupIntents if you are either using a Free-tier or give a Free trial. When someone enters a credit card, for one of these subscriptions, you will see a <code>pending_setup_intent</code> on the <a target="_blank" href="https://stripe.com/docs/api/subscriptions/object#subscription_object-pending_setup_intent">subscription object</a>. The SetupIntent's <code>client_secret</code> should be passed to the front-end so that Stripe.js can start the 3DS2 authentication flow.</p>
<h2 id="heading-using-stripejs-handlecardsetup-with-the-setupintent"><strong>Using Stripe.js handleCardSetup with the SetupIntent</strong></h2>
<p>This is  basically identical to how we handled the PaymentIntent, except we call handleCardSetup instead</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> stripe.handleCardSetup(<span class="hljs-string">'{SETUP_INTENT_CLIENT_SECRET}'</span>)
</code></pre>
<p>You can see a SetupIntent SCA Flow in action below.</p>
<div class="embed-wrapper">
        <iframe width="100%" height="350" src="https://codepen.io/bsears/embed/RwwGyYw?editors=1111" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="CodePen embed" scrolling="no" allowtransparency="true" allowfullscreen="true" loading="lazy"></iframe></div>
<p>Once authentication completes, the customer can be moved to a paid plan later or have their card charged after a free trial is over.</p>
<h1 id="heading-no-code-alternative"><strong>No-code alternative</strong></h1>
<p>If you are looking for an SCA-compliant solution for Stripe Billing without having to deal with the 3DS2 integration development, check out <a target="_blank" href="https://servicebot.io">Servicebot</a>. We provide a drop-in UI for SaaS companies using Stripe, which is SCA-compliant out-of-the-box! Want to see it in action? Check out <a target="_blank" href="https://dashboard.servicebot.io/examples/signup-embed/0">this demo</a> and use the test card <code>4000002760003184</code> (any Expiration and CVC).</p>
<p><a href="https://servicebot.io"><img src="https://i.imgur.com/QJkpyHN.png" width="1600" height="753" alt="QJkpyHN" loading="lazy"></a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
