Building a fitness website is not only about workouts, animations, and a good-looking UI.
If you want to turn a fitness platform into a real product, you also need a way for users to purchase memberships.
While building my React-based fitness website, I decided to integrate Razorpay so users could join different membership plans directly from the website.
In this article, I'll show how I implemented the payment flow, connected React with a Node.js backend, created Razorpay orders securely, and verified payments on the server.
The project uses:
- React + Vite
- Node.js + Express
- Razorpay
- Supabase
- Vercel
The goal was to create a simple flow:
Choose Plan → Create Order → Open Razorpay Checkout → Complete Payment → Verify Payment
1. Setting Up the Membership Plans
For the fitness website, I created three simple membership plans:
| Plan | Price |
|---|---|
| Basic | ₹99 |
| Premium | ₹149 |
| Elite | ₹199 |
Each plan has its own Join Now button.
When a user clicks the button, the frontend sends the selected plan to the backend instead of directly trusting the price from the browser.
This is important because payment amounts should be controlled on the server side.
For example, my backend keeps the plan prices like this:
const MEMBERSHIP_PLANS = {
basic: {
name: "Basic",
amount: 9900
},
premium: {
name: "Premium",
amount: 14900
},
elite: {
name: "Elite",
amount: 19900
}
};
## 2. Creating a Razorpay Order on the Backend
I did not create the Razorpay order directly from the React frontend.
Instead, the frontend sends the selected plan to my Node.js + Express backend.
The backend then creates the Razorpay order using the server-side amount configured for that plan.
The basic flow looks like this:
text
React Frontend
↓
Select Membership
↓
POST /api/create-order
↓
Node.js + Express
↓
Razorpay API
↓
Order ID
↓
Razorpay Checkout
app.post("/api/create-order", async (req, res) => {
try {
const { planId } = req.body;
const plan = MEMBERSHIP_PLANS[planId];
if (!plan) {
return res.status(400).json({
error: "Invalid membership plan"
});
}
const razorpay = getRazorpayClient();
const order = await razorpay.orders.create({
amount: plan.amount,
currency: "INR",
receipt: `receipt_${Date.now()}`
});
res.json({
orderId: order.id,
amount: order.amount,
currency: order.currency,
planName: plan.name
});
} catch (error) {
console.error(error);
res.status(500).json({
error: "Unable to create order"
});
}
});
3. Opening Razorpay Checkout from React
Once the backend creates the order, it returns the Razorpay orderId to the React frontend.
The frontend then uses the Razorpay Checkout SDK to open the payment window.
The flow is:
User clicks "Join Now"
↓
React calls /api/create-order
↓
Backend creates Razorpay Order
↓
Frontend receives Order ID
↓
Razorpay Checkout opens
↓
User selects payment method
↓
Payment is completed
const handlePayment = async (planId) => {
const response = await fetch("/api/create-order", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ planId })
});
const order = await response.json();
const options = {
key: import.meta.env.VITE_RAZORPAY_KEY_ID,
amount: order.amount,
currency: "INR",
name: "Official ASForge",
description: `${order.planName} Membership`,
order_id: order.orderId,
handler: async (paymentResponse) => {
// Payment verification happens here
}
};
const razorpay = new window.Razorpay(options);
razorpay.open();
};
## 4. Verifying the Payment Securely
Opening the Razorpay Checkout is only one part of the payment process.
After a successful payment, Razorpay returns payment details such as:
- `razorpay_payment_id`
- `razorpay_order_id`
- `razorpay_signature`
I send these values back to my backend for verification.
The backend generates an HMAC SHA256 signature using the Razorpay Key Secret and compares it with the signature received from Razorpay.
A simplified implementation looks like this:
js
app.post("/api/verify-payment", (req, res) => {
try {
const {
razorpay_order_id,
razorpay_payment_id,
razorpay_signature
} = req.body;
const generatedSignature = crypto
.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET)
.update(
`${razorpay_order_id}|${razorpay_payment_id}`
)
.digest("hex");
const isValid =
generatedSignature === razorpay_signature;
if (!isValid) {
return res.status(400).json({
success: false,
error: "Invalid payment signature"
});
}
res.json({
success: true,
message: "Payment verified successfully"
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
error: "Payment verification failed"
});
}
});
Step 8 — Keeping Secrets Safe
## 5. Keeping Razorpay Secrets Safe
One of the most important parts of the integration was keeping the Razorpay credentials secure.
I used environment variables for the backend credentials:
env
RAZORPAY_KEY_ID=your_key_id
RAZORPAY_KEY_SECRET=your_secret
VITE_RAZORPAY_KEY_ID=your_key_id
Step 9 — Complete Payment Flow
## 6. The Complete Payment Flow
After putting everything together, the complete membership flow looks like this:
text
User
↓
Selects a Plan
↓
Clicks "Join Now"
↓
React Frontend
↓
POST /api/create-order
↓
Express Backend
↓
Razorpay API
↓
Order ID
↓
Razorpay Checkout
↓
User Pays
↓
Payment Details
↓
POST /api/verify-payment
↓
Server-Side Verification
↓
Payment Success
Step 10 — Supabase
## 7. Connecting Payments with Supabase
After getting the Razorpay payment flow working, I also connected Supabase to the project.
Supabase can be used to store application data such as:
- User information
- Memberships
- Payment records
- Workout plans
- Diet plans
- Progress
- AI usage
- Website events
For example, after a verified payment, a payment record can be stored with information such as:
text
user_id
plan
amount
razorpay_order_id
razorpay_payment_id
payment_status
created_at
Step 11 — Vercel Deployment
## 8. Deploying the Payment System
My frontend is built with Vite and deployed using Vercel.
During local development, the React application communicates with my local Express server.
For production, the payment endpoints need to be available from the deployed application as well.
The production architecture can look like:
text
React + Vite
↓
Vercel
↓
/api/create-order
/api/verify-payment
↓
Razorpay
Step 12 — Problems I Faced
## 9. Problems I Faced During Integration
While integrating Razorpay, I ran into a few common issues.
### Duplicate function declarations
While modifying the React component, I accidentally declared the same `handlePayment` function more than once.
This resulted in an error similar to:
text
Identifier 'handlePayment' has already been declared
11. What I Learned
This project taught me that adding payments to a website is more than simply opening a checkout window.
The important concepts I learned were:
- Never trust payment amounts coming directly from the frontend.
- Create orders on the backend.
- Keep secret keys on the server.
- Verify Razorpay signatures server-side.
- Use environment variables for sensitive credentials.
- Separate development and production API configurations.
- Store payment records only after successful verification.
- Think about security while designing the payment flow, not after finishing the UI.
What's Next?
In the next part, I'll explore how I built a multilingual AI fitness coach that can interact with users in multiple languages using Gemini.
Thanks for reading! 💪
If you enjoyed the article, feel free to follow me for more posts about:
- React
- AI
- Three.js
- Full-stack development
- Fitness technology
Build. Train. Improve.
— Aman Singh
Top comments (0)