Skip to content

Cannot POST / error when trying to create a checkout session with Stripe + React + Express

I am using Stripe’s pre-built checkout method to order and pay for products on my Reactjs app. I recently developed a basic shopping cart and now I am trying to create a “Go To Checkout” input form that would allow the user to send the products in the cart to my express server POST route where express will redirect the user to the stripe checkout page. The issue is that the moment when I press on the form input to make the HTTP post request, I get a “Cannot POST /cart” response with no error messages.

The interesting is that by using Postman I am able to reach the POST route. Also, I have other routes set up that are used to GET data from other APIs and they are working fine, but for some reason, this POST route is not working no matter what I am doing.

Any suggestions would be welcome.

Below are the relevant files and the code found in them.

cart-page.js – this is the code that is responsible for the cart and has the code for the form that is supposed to make the HTTP request when pressed (‘const goToCheckout’)

import React from "react";

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

//* Allows Stripe to authentificate our API requests with our key
const stripePublishableKey = nodeEnv ? process.env.REACT_APP_stripe_dev_publishable_key : process.env.REACT_APP_stripe_prod_pubishable_key;
const stripe = require('stripe')(stripePublishableKey);

const CartPage = (props) => {

    const { cart, onAdd, onRemove } = props;

    const productTotal = cart.reduce((a, c) => a + c.unit_amount * c.qty, 0) // default value 0
    const taxTotal = <p>Tax is included in the price.</p>
    const shippingTotal = <p>You can choose your shipping options at checkout.</p>
    const totalCost = productTotal;

    const checkoutData = cart.map(item => (
        { 
            price: item.id, 
            quantity: item.qty,
        }
    ));

    const goToCheckout = async () => {

        // Call your backend to create the Checkout Session
        await fetch('/create-checkout-session', {
            method: "POST",
            headers: {
                    "Content-Type": "application/json"
                },
            body: JSON.stringify({
                items: [
                    checkoutData
                ]
            }),
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(session) {
            return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function(result) {
            // If `redirectToCheckout` fails due to a browser or network
            // error, you should display the localized error message to your
            // customer using `error.message`.
            if (result.error) {
                alert(result.error.message);
            }
        })
        .catch((error) => {
            console.error(error);
        });
    };

    const currencyFormatter = new Intl.NumberFormat('en-gb', {
        style:"currency", 
        currency:"GBP"
    }) 

    return (
        <main>

            <h1>Your Cart</h1>

            {cart.length === 0 && <p>Your Cart is Empty...</p>}

            {cart.map((item) => (
                <section className='cart-item' key={item.product.id}>
                    <h4>{item.product.name}</h4>
                        
                    <section className='cart-item-buttons'>
                        <button onClick={() => onAdd(item)}>+</button>
                        <button onClick={() => onRemove(item)}>-</button>
                    </section>
                        
                    <p>{item.qty} * {currencyFormatter.format(item.unit_amount / 100)}</p>
                </section>
            ))}

            {cart.length !== 0 && (
                <section>
                    <p>Total Product Price: {currencyFormatter.format(productTotal / 100)}</p>
                    <p>Toal Tax: {taxTotal}</p>
                    <p>Shipping Costs: {shippingTotal}</p>
                    <p><strong>Total Costs: {currencyFormatter.format(totalCost / 100)}</strong></p>
                </section>
            )}

            {cart.length > 0 && (
                <section>
                    <p>ADD CHECKOUT BUTTON</p>

                    <form method='POST' action={goToCheckout}>
                        <input type='submit' value='Go To Checkout' />
                    </form>

                </section>
            )}

        </main>
    );
};

export default CartPage;

createCheskoutSession.js – this file contains all the code that is responsible for the ‘/create-checkout-session’ route. It is supposed to accept the request and using the Stripe API creates the checkout page that will be populated with the cart items. From what I understand, my POST request does not reach this point. I think…

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

const YOUR_DOMAIN = nodeEnv ? process.env.REACT_APP_dev_domain : process.env.REACT_APP_prod_domain;

//* Allows Stripe to authentificate our API requests with our key
const stripeSecretKey = nodeEnv ? process.env.REACT_APP_stripe_dev_secret_key : process.env.REACT_APP_stripe_prod_secret_key;
const stripe = require('stripe')(stripeSecretKey);

//* To override the API version, provide the apiVersion option:
//*  Before upgrading your API version in the Dashboard, review both the API changelog and the library changelog.
/*
const stripe = require('stripe')(stripeSecretKey, {
  apiVersion: '2020-08-27',
});
*/

//* After creating a Checkout Session, redirect your customer to the URL returned in the response.
//* Add an endpoint on your server that creates a Checkout Session. A Checkout Session controls what your customer sees 
//* in the Stripe-hosted payment page such as line items, the order amount and currency, and acceptable payment methods.

const createCheckoutSession = async (req, res) => {
    
    const session = await stripe.checkout.sessions.create({

    //* Prefill customer data
    //* Use customer_email to prefill the customer’s email address in the email input field. You can also pass a
    //*  Customer ID to customer field to prefill the email address field with the email stored on the Customer.
    //* customer_email: '[email protected]',
        
    //* Pick a submit button // Configure the copy displayed on the Checkout submit button by setting the submit_type. There are four different submit types.
        submit_type: 'donate',
        
        /*
        Collect billing and shipping details
        Use billing_address_collection and shipping_address_collection to collect your customer’s address. 
        shipping_address_collection requires a list of allowed_countries. Checkout displays the list of allowed
        countries in a dropdown on the page.
        */
            
        billing_address_collection: 'auto',
        shipping_address_collection: {
            allowed_countries: ['US', 'CA', 'LV'],
        },

        /*
        Define a product to sell
        Always keep sensitive information about your product inventory, like price and availability, on your server 
        to prevent customer manipulation from the client. Define product information when you create the Checkout
        Session using predefined price IDs or on the fly with price_data.
        */
        /*
            line_items: [
                {
                    price: 'price_1JsxdVBSHV1ZLiWD7n4PcKf9',
                    quantity: 1,
                },
            ],
        */
        //* Provide the exact Price ID (e.g. pr_1234) of the product you want to sell
/*
            line_items: [
                cartItems.map(item => {
                    return {
                        price: item.price,
                        quantity: item.quantity,
                    }
                })
            ],
*/
            line_items: req.body.items,
            
            /*
            req.body.items.map(item => {
                return {
                    price: item.id,
                    quantity: item.qty,
                },
            },
            */

        //* When you pass multiple payment methods, Checkout dynamically displays them to prioritize what’s most 
        //* relevant to the customer. Apple Pay and Google Pay are included automatically when you include card in 
        //* payment_method_types.
        //* Apple Pay and Google Pay are enabled by default and automatically appear in Checkout when a customer 
        //* uses a supported device and has saved at least one card in their digital wallet. 
            payment_method_types: [
                'card',
            ],

        //* Choose the mode
        //* Checkout has three modes: payment, subscription, or setup. Use payment mode for one-time purchases.
        //*  Learn more about subscription and setup modes in the docs.
            mode: 'payment',

        //* Supply success and cancel URLs
        //* Specify URLs for success and cancel pages—make sure they are publicly accessible so Stripe can redirect 
        //* customers to them. You can also handle both the success and canceled states with the same URL.
            success_url: `${YOUR_DOMAIN}/stripe/stripe-success.html`, //! Change
            cancel_url: `${YOUR_DOMAIN}/stripe/stripe-cancel.html`, //! Change
        //* Activate Stripe Tax to monitor your tax obligations, automatically collect tax, and access the reports you need to file returns.
        //* automatic_tax: {enabled: true},
    });

    //* Redirect to Checkout
    //* After creating the session, redirect your customer to the Checkout page’s URL returned in the response.
  
    res.redirect(303, session.url);

};

module.exports = createCheckoutSession;

server.js – this is most of the code that is responsible for managing my express server. There is another file that uses Router to define the express endpoints/routes.

const express = require('express');
const helmet = require('helmet'); 
const cors = require('cors'); 
const path = require('path'); // Allows to access files through the server in our filesystem

/**
**  ------------- GENERAL SETUP -------------
*/

// Provides access to variables from the .env file by using process.env.REACT_APP_variable_name
    require('dotenv').config();

    const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';
    const devPort = process.env.REACT_APP_server_dev_port;
    const prodPort = process.env.REACT_APP_server_prod_port;

//* Creates the Express server instance as "app" 
    const app = express();

//* MIDDLEWARE
// Called BETWEEN processing the Request and sending the Response in your application method.
    app.use(cors()); // To allow cross origin conections (Allows our React app to make HTTP requests to Express application)
    app.use(helmet()); // Sets many http headers to make them more secure
    app.use(express.static(path.join(__dirname, 'public'))); // To load static files or client files from here http://localhost:3000/images/kitten.jpg
    // Instead of using body-parser middleware, use the new Express implementation of the same thing
        app.use(express.json()); // To recognize the incoming Request Object (req.body) as a JSON Object
        app.use(express.urlencoded({ extended: false })); // To recognize the incoming Request Object as strings or arrays

/**
** -------------- SERVER ----------------
*/
       
// Determines the PORT and enables LISTENing for requests on the PORT (http://localhost:8000)

    const PORT = nodeEnv ? devPort : prodPort;
       
    app.listen(PORT, () => {
      console.debug(`Server is listening at http://localhost:${PORT}`);
    });
  
/**
** ------- ROUTES / ENDPOINTS ---------
*/

// Go to /test to make sure the basic API functioning is working properly
    app.get('/test', (req, res) => {
        res.status(200).send('The Basic API endpoints are working.')
    });

// Imports all of the routes from ./routes/index.js
    app.use(require('./routes/allRoutes'));

Update 1#

This is what my browser’s request console is showing

Request URL: http://localhost:3000/cart
Request Method: POST
Status Code: 404 Not Found
Remote Address: 127.0.0.1:3000
Referrer Policy: strict-origin-when-cross-origin
access-control-allow-origin: *
connection: close
content-length: 144
content-security-policy: default-src 'none'
content-type: text/html; charset=utf-8
date: Fri, 26 Nov 2021 05:25:35 GMT
expect-ct: max-age=0
referrer-policy: no-referrer
strict-transport-security: max-age=15552000; includeSubDomains
Vary: Accept-Encoding
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
X-Powered-By: Express
x-xss-protection: 0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: lv-LV,lv;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
DNT: 1
Host: localhost:3000
Origin: http://localhost:3000
Referer: http://localhost:3000/cart
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1

Update 2#

I’ve had some progress. I was able to call the goToCheckout() function by changing…

<form method='POST' action={goToCheckout}>
     <input type='submit' value='Go To Checkout' />
</form>

To…

<form type="button" onSubmit={goToCheckout}>
      <button>
          Go To Checkout
      </button>
</form>

The only issue now is that after I press the checkout button, the code in the goToCheckout function tires to execute, but I get redirected to the cart page, the only difference now is that if before the URL was “http://localhost:3000/cart” now it is “http://localhost:3000/cart?”. I think this is because the button is in a form (but that is the only way I have been able to figure out how to call the goToCheckout() function). I tried to do add event.preventDefault() into the function, but that did not seem to do anything.

Does anyone have an idea why the fetch code is not properly executing and redirecting the user to the stripe checkout page but instead just bringing me back to the same URL with a ? without any params attached to it.

Update 3#

When I access the same route from Postman, I am able to get the Stripe checkout URL to redirect the user to the Checkout page so that they can pay for the products in test mode (for now).

Meaning that the route itself works as intended.

Now I only have to figure out how to stop the page to refresh when I use the form to call my fetch function, adding a “?” sign at the end of the URL, and execute the fetch just like Postman does it.

If anyone knows how to do that without using a form, which is what I am using now, that would be a great help. I tried using a , but no matter how I added the goToCheckout(); function to onClick/action, etc. the functions would not call.

Answer

This was a long time ago but I guess I might as well answer my own question.

  1. Had to change the internal logic of the goToCheckout function:
    const goToCheckout = (e) => {
        e.preventDefault();
        fetch(`${customProxy}/create-checkout-session`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ items: checkoutData}),
        })
        .then(res => {
            if (res.ok) return res.json()
            return res.json().then(json => Promise.reject(json))
        })
        .then(({ url }) => {
            window.location = url
        })
        .catch((error) => {
            // console.error(error);
            alert("Create Stripe checkout:" + error);
        });
    };
  1. Changed the HTML for the goToCheckout button
<button className='go-to-checkout-button' onClick={goToCheckout}>
       Go To Checkout
</button>