Modern web applications often require robust payment processing capabilities. Stripe has long been the go-to solution for developers seeking a reliable and developer-friendly payment API. Meanwhile, Remix Run has emerged as a powerful React-based framework that provides an excellent foundation for building dynamic, server-rendered applications. Combining these technologies creates an optimal stack for implementing payment systems in your web applications.
This guide walks you through integrating Stripe with Remix Run to create a secure, performant payment system in 2025.
Before diving into implementation details, let's explore why this combination works so well:
Let's start by setting up a new Remix project with Stripe integration:
npx create-remix@latest my-stripe-app
cd my-stripe-app
npm install stripe @stripe/stripe-js @stripe/react-stripe-js
Create a .env
file in your project root to store your Stripe API keys:
STRIPE_PUBLIC_KEY=pk_test_your_public_key
STRIPE_SECRET_KEY=sk_test_your_secret_key
Remember to add this file to your .gitignore
to keep your keys secure.
First, let's create a Stripe provider component to initialize the Stripe instance:
// app/components/StripeProvider.jsx
import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { useLoaderData } from '@remix-run/react';
export default function StripeProvider({ children }) {
const { stripePublicKey } = useLoaderData();
const [stripePromise, setStripePromise] = useState(null);
useEffect(() => {
if (stripePublicKey) {
setStripePromise(loadStripe(stripePublicKey));
}
}, [stripePublicKey]);
return (
<Elements stripe={stripePromise}>
{children}
</Elements>
);
}
Create a payment route in your Remix application:
// app/routes/checkout.jsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { redirect } from '@remix-run/node';
import Stripe from 'stripe';
import StripeProvider from '~/components/StripeProvider';
import PaymentForm from '~/components/PaymentForm';
export const loader = async () => {
return json({
stripePublicKey: process.env.STRIPE_PUBLIC_KEY,
});
};
export const action = async ({ request }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const formData = await request.formData();
const paymentMethodId = formData.get('paymentMethodId');
const amount = formData.get('amount');
const currency = formData.get('currency') || 'usd';
try {
// Create a payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(parseFloat(amount) * 100), // Convert to cents
currency,
payment_method: paymentMethodId,
confirm: true,
return_url: `${new URL(request.url).origin}/payment-success`,
});
// Handle next steps based on PaymentIntent status
if (paymentIntent.status === 'succeeded') {
return redirect('/payment-success');
} else if (paymentIntent.status === 'requires_action') {
return json({ clientSecret: paymentIntent.client_secret, requiresAction: true });
} else {
return json({ error: 'Payment failed' }, { status: 400 });
}
} catch (error) {
return json({ error: error.message }, { status: 400 });
}
};
export default function Checkout() {
const data = useLoaderData();
return (
<StripeProvider>
<div className="checkout-container">
<h1>Complete Your Payment</h1>
<PaymentForm />
</div>
</StripeProvider>
);
}
Now, let's create a payment form component:
// app/components/PaymentForm.jsx
import { useState } from 'react';
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js';
import { Form, useSubmit, useActionData } from '@remix-run/react';
export default function PaymentForm() {
const stripe = useStripe();
const elements = useElements();
const submit = useSubmit();
const actionData = useActionData();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
setProcessing(true);
setError(null);
const cardElement = elements.getElement(CardElement);
// Create payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
setError(error.message);
setProcessing(false);
return;
}
// Create form data to submit to action
const formData = new FormData();
formData.append('paymentMethodId', paymentMethod.id);
formData.append('amount', '29.99'); // Replace with dynamic amount
formData.append('currency', 'usd');
submit(formData, { method: 'post' });
};
// Handle required additional actions (3D Secure, etc.)
const handleRequiresAction = async () => {
if (actionData?.requiresAction && actionData?.clientSecret) {
setProcessing(true);
const { error } = await stripe.confirmCardPayment(actionData.clientSecret);
if (error) {
setError(error.message);
}
setProcessing(false);
}
};
// Call handleRequiresAction when actionData changes
useEffect(() => {
if (actionData?.requiresAction) {
handleRequiresAction();
}
}, [actionData]);
return (
<Form onSubmit={handleSubmit} className="payment-form">
<div className="form-row">
<label htmlFor="card-element">Credit or debit card</label>
<CardElement id="card-element" options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#9e2146',
},
},
}} />
</div>
{error && <div className="error-message">{error}</div>}
{actionData?.error && <div className="error-message">{actionData.error}</div>}
<button
type="submit"
disabled={!stripe || processing}
className="payment-button"
>
{processing ? 'Processing...' : 'Pay $29.99'}
</button>
</Form>
);
}
Don't forget to create routes for successful payments and handling errors:
// app/routes/payment-success.jsx
export default function PaymentSuccess() {
return (
<div className="success-container">
<h1>Payment Successful!</h1>
<p>Thank you for your purchase. You will receive a confirmation email shortly.</p>
<a href="/" className="button">Return to Home</a>
</div>
);
}
For a production-ready system, you'll want to implement Stripe webhooks to handle asynchronous events:
// app/routes/api/stripe-webhook.jsx
import { json } from '@remix-run/node';
import Stripe from 'stripe';
export const action = async ({ request }) => {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const signature = request.headers.get('stripe-signature');
if (!signature) {
return json({ error: 'Missing Stripe signature' }, { status: 400 });
}
try {
const text = await request.text();
const event = stripe.webhooks.constructEvent(
text,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
// Handle different event types
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
// Update your database, send confirmation emails, etc.
console.log(`PaymentIntent ${paymentIntent.id} succeeded`);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log(`Payment failed: ${failedPayment.last_payment_error?.message}`);
// Handle the failure
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return json({ received: true });
} catch (err) {
return json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
};
When integrating Stripe with Remix, keep these best practices in mind:
export function ErrorBoundary() {
return (
<div className="error-container">
<h1>Payment Error</h1>
<p>There was an error processing your payment. Please try again or contact support.</p>
</div>
);
}
Consider these enhancements to improve the payment experience:
If you encounter CORS errors, ensure your Remix server is properly configured to handle Stripe's requests.
If payment confirmations seem delayed, check that your webhook implementation is correctly set up and your server can receive Stripe's webhook events.
Remember that test mode and live mode in Stripe use completely different sets of API keys. Issues can arise when mixing these environments.
Integrating Stripe with Remix Run creates a powerful foundation for handling payments in modern web applications. The server-side rendering capabilities of Remix paired with Stripe's robust API offer numerous advantages for creating secure, user-friendly payment experiences.
By following this guide, you've learned how to set up a basic payment system with Stripe in a Remix application. As you build out your application, remember to consult Stripe's documentation for the latest best practices and new features that can enhance your payment system.
Happy coding!
Start your journey with us today and get access to our resources and tools.