Lemon Squeezy x Supabase x Next JS - Part 1

Fetching a Lemon Squeezy Product and the linked Product Variants


The complete guide is a work in progress, this first part is completed.

If you are looking for a guide to help you out with setting up your very own Subscription using Next.JS, Lemon Squeezy and Supabase, you have come to the right place. In this first guide, we will look at how you can fetch products from Lemon Squeezy, so that you can populate your pricing table!

This guide is specifically created for Subscriptions with one product and multiple variants. You can see the difference in pricing strategy here.

The goal of this guide is that by the end you will be able to fetch your own products using Next.js 13 App Router.

Why Lemon Squeezy

Lemon Squeezy is a MoR, also known as Merchant of Record. In short, if you use Lemon Squeezy you don't have to handle global tax yourself. Yes, I thought so too. That is super useful and makes my life (and hopefully yours soon) just that much easier.

Setting up the Lemon Squeezy connection

This guide was fully created using the Lemon Squeezy API Documentation, this documentation is subject to change. If this guide is not up to date anymore, please let me know! The guide is also specifically created for Next.js 13, as we are using the "use server".

To be able to set this up yourself, you will need to have at least one product, with one variant!

Setting up the .env file

Within your .env file you will need to set up the following information:

./.env.local
    LEMON_SQUEEZY_API_KEY=
    LEMON_SQUEEZY_WEBHOOK_SECRET=
    STORE_ID=
    PRODUCT_ID=

If you are unsure where to find this information, you can find more about that here.

Setting up your Next.js project

This guide asumes you already have a working project, with Next.js 13 installed and dependencies set. From here we are going to create a seperate page, where you can select one of the variants of your subscription.

Subscription setup
   ├── app
   │   ├── subscription
   │   │   ├── page.tsx
   │   │   ├── pricing.tsx
   │   │   ├── pricing-display.tsx
   │   │   ├── variants.tsx
   ├── and other pages

In the subscription folder, we will created 4 .tsx files. Each file has a function:

  1. page.tsx - the page itself! yourapp.com/subscription
  2. pricing.tsx - component that fetches the variants and is used to retrieve the environment variable for Product STORE_ID
  3. pricing-display.tsx - you could use pricing.tsx to render out the div, I prefer creating a component with one function
  4. variants.tsx - requesting the information from Lemon Squeezy

Lets start in reverse order: variants.tsx

Retrieving the information from Lemon Squeezy

Lets have a look at the API documentation from Lemon Squeezy for Products & Variants.

GET Variants
    curl "https://api.lemonsqueezy.com/v1/variants"
        -H 'Accept: application/vnd.api+json'
        -H 'Content-Type: application/vnd.api+json'
        -H 'Authorization: Bearer {api_key}'

In the GET Variants example, if we use this method, we will receive all variants, of all products. As we want to receive the variants for a specific product, we are going with the second option:

GET Variants for a Product
    curl "https://api.lemonsqueezy.com/v1/variants?filter[product_id]=12345"
        -H 'Accept: application/vnd.api+json'
        -H 'Content-Type: application/vnd.api+json'
        -H 'Authorization: Bearer {api_key}'

If you look closely, we can already spot two different environment variables that we are going to use for this:

  • LEMON_SQUEEZY_API_KEY: '-H Authorization: Bearer {api_key}'
  • PRODUCT_ID: '?filter[product_id]=12345'

Now that we know what kind of request we need to send, we can start working on the first file.

This is what variants.tsx will look like when we are finished:

./subscription/variants.tsx
    "use server"
 
    const lemonSqueezyBaseUrl = 'https://api.lemonsqueezy.com/v1';
    const lemonSqueezyApiKey = process.env.LEMON_SQUEEZY_API_KEY;
 
    if (!lemonSqueezyApiKey) throw new Error("No LEMON_SQUEEZY_API_KEY
     environment variable set");
 
    function createHeaders() {
        const headers = new Headers();
        headers.append('Accept', 'application/vnd.api+json');
        headers.append('Content-Type', 'application/vnd.api+json');
        headers.append('Authorization', `Bearer ${lemonSqueezyApiKey}`);
        return headers;
    }
 
    function createRequestOptions(method: string, headers: Headers): RequestInit {
        return {
            method,
            headers,
            redirect: 'follow',
            cache: "no-store"
        };
    }
 
 
    export async function getProductVariants(productId: string) {
        const url = `${lemonSqueezyBaseUrl}/variants?filter[product_id]=${productId}`;
        const headers = createHeaders();
        const requestOptions = createRequestOptions('GET', headers);
 
        const response: Response = await fetch(url, requestOptions);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
 
        const data = await response.json();
        return data;
    }

Let's break it down.

Variables, headers & request options

The starting section of the code contains specific information to be able to create the request, which will be used later in getProductVariants function.

The first thing that you probably noticed, is the following: "use server". To be able to use server actions, we need to inform Next.js we are going to require these functions.

Note that this will only be available in async functions, for non async or "use client" components, this will not work.

    const lemonSqueezyBaseUrl = 'https://api.lemonsqueezy.com/v1';
    const lemonSqueezyApiKey = process.env.LEMON_SQUEEZY_API_KEY;
 
    if (!lemonSqueezyApiKey) throw new Error("No LEMON_SQUEEZY_API_KEY
        environment variable set");

In the first part, we want to create the url, and we want to retrieve the API key for Lemon Squeezy. We want this information, as we will need to use it to create the GET request for the /v1/Variants of Lemon Squeezy as we have seen earlier.

  function createHeaders() {
    const headers = new Headers();
 
    return headers;
  }

When sending out an API request, the receiving party may require headers. This is to make sure that the integrated files matches the required file for integration. The function we use is Headers() constructor, which is default functionality for JavaScript.

Now that we created the variable headers, we can add information to it. As we have seen before, we need three headers:

  1. Accept
  2. Content-Type
  3. Authorization
  headers.append("Accept", "application/vnd.api+json");
  headers.append("Content-Type", "application/vnd.api+json");
  headers.append("Authorization", `Bearer ${lemonSqueezyApiKey}`);

If we combine the two previous ones, we get make the headers available, so that we can use it in the request to Lemon Squeezy:

  function createHeaders() {
    const headers = new Headers();
    headers.append("Accept", "application/vnd.api+json");
    headers.append("Content-Type", "application/vnd.api+json");
    headers.append("Authorization", `Bearer ${lemonSqueezyApiKey}`);
    return headers;
  }

Before we can send the request, we need to set the request options too. The request options, will include the method and the headers for our request.

Let's break down the function. The parameters are method and headers, as we want to only fill those within the request. The structure is RequestInit, as our options will be structured in that manner.

If we have more requests we can also make this a reusable component. That is also how I structured the request. Inside the function, we have the RequestInit object with the structure:

  • Method - in this example we will use GET
  • Headers - As we just created
  • Redirect - Set to follow or return
  • Cache - Indicating how to proceed with browser cache, store the information or not.
  function createRequestOptions(method: string, headers: Headers): RequestInit {
    return {
      method,
      headers,
      redirect: "follow",
      cache: "no-store",
    };
  }

getProductVariants

Now that we have all the required information set up, we can start the request towards Lemon Squeezy. To be able to send out the request, we need to have all prior information, including the product id. The product id can be a variable, based on the setup of your project. This means that this can't be requested from the start, directly.

Lets break down the function getProductVariants

async function getProductVariants
  export async function getProductVariants(productId: string) {
    const url = `${lemonSqueezyBaseUrl}/variants?filter[product_id]=${productId}`;
    const headers = createHeaders();
    const requestOptions = createRequestOptions("GET", headers);
 
    const response: Response = await fetch(url, requestOptions);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const data = await response.json();
    return data;
  }

First we have the URL variable: const url = ${lemonSqueezyBaseUrl}/variants?filter[product_id]=${productId}

  • ${lemonSqueezyBaseUrl}
    • this URL has been defined at the top of the code: 'https://api.lemonsqueezy.com/v1'
  • /variants?filter[product_id]=
    • the required information to be able to request the variants for a productr
  • ${productId}
    • received from the parent component, where the product id is defined

The second part is importing the headers, we can do so by adding the variable headers, and assigning it to the function createHeaders, which returns the headers we have created prior. The third and final part of the setup withing getProductVariants is the requestOptions, which we have defined earlier. Here we pass it the values 'GET' for a GET Request, and the headers we have created before.

The response has the type Response. We set the response equal to the fetch query for the URL and the requestOptions we have defined earlier. When we receive an error, we will throw an error, which causes the request to stop.

const response: Response = await fetch(url, requestOptions);
if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

If all works correctly, we then create a new variable data, which we set equal to awaiting the response from Lemon Squeezy. Make sure to also return the value back, so that you can use it in your application!

Finished request

Now that we have the first request, we can check if everything is working. You can manually start this request if you would have created an /api/route, but since we created a server component, we will have to wait until we send out the request to Lemon Squeezy by clicking a button.

But no worries, we can still request the data from Lemon Squeezy using either Postman or another method which you prefer to verify that it is working

postman get lemon squeezy products

Like what you see? Do you prefer to have a ready made app instead of building the integration yourself?

No worries, we have got you covered.

With Supaboost, you will be able to skip at least 30 days development time, by having these features readily available:

  • Auth
  • Lemon Squeezy integration
  • Safe development with Typescript
  • Next.js App Router
  • Supabase SQL scripts
  • And much more.

Get Supaboost

View

Next step

Click here to continue to Rendering

View