I was recently building a project that required payment processing, but when I tried to integrate Razorpay, I realized most available tutorials were outdated and still focused on the legacy Pages router. I spent some time adapting the implementation to work correctly with Server Components and the modern App Router architecture, and here is the configuration that finally worked for me.
0. Prerequisites
Install the Razorpay NPM package:
npm install razorpay
1. Create API Route
Create an API route in app/api/order/create/route.ts:
import { NextResponse } from "next/server"
import Razorpay from "razorpay"
import { v4 as uuid } from "uuid"
const instance = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID,
key_secret: process.env.RAZORPAY_KEY_SECRET,
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const totalAmount = Number(searchParams.get("amount")) // in paisa
const amount = totalAmount * 100
const options = {
amount: amount.toString(),
currency: "INR",
receipt: uuid(),
}
const order = await instance.orders.create(options)
return NextResponse.json({ message: "success", order })
}
Create one more route in app/api/order/verify/route.ts:
import { NextResponse } from "next/server"
import Razorpay from "razorpay"
import crypto from "crypto"
import Order from "@/models/OrderModel"
import { v4 as uuid } from "uuid"
import { connectDB } from "@/lib/mongodb"
const instance = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID,
key_secret: process.env.RAZORPAY_KEY_SECRET,
})
export async function POST(req, res) {
const { razorpayOrderId, razorpaySignature, razorpayPaymentId, email } =
await req.json()
const body = razorpayOrderId + "|" + razorpayPaymentId
const expectedSignature = crypto
.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET)
.update(body.toString())
.digest("hex")
const isAuthentic = expectedSignature === razorpaySignature
if (!isAuthentic) {
return NextResponse.json(
{ message: "invalid payment signature", error: true },
{ status: 400 }
)
}
// connect db and update data
await connectDB()
await Order.findOneAndUpdate({ email: email }, { hasPaid: true })
return NextResponse.json(
{ message: "payment success", error: false },
{ status: 200 }
)
}
2. Add Razorpay Script to Root Layout
Add the Razorpay script to the root layout in app/layout.tsx:
import Script from "next/script"
import "./globals.css"
export default function RootLayout({ children }) {
return (
<>
<html lang="en">
<body>{children}</body>
</html>
<Script src="https://checkout.razorpay.com/v1/checkout.js" />
</>
)
}
3. Create Payment Button Component
Create a payment button component in app/components/PaymentButton.tsx:
"use client"
import React, { Suspense, useState } from "react"
import { useRouter } from "next/navigation"
import Loading from "@/app/loading"
import { useSession } from "next-auth/react"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const PaymentButton = ({ amount }) => {
const { userData } = useSession()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const makePayment = async () => {
setIsLoading(true)
// make an endpoint to get this key
const key = "rzp_test_M******Pw5***n"
const data = await fetch("/api/order/create?amount=" + amount)
const { order } = await data?.json()
const options = {
key: key,
name: userData.user?.email,
currency: order.currency,
amount: order.amount,
order_id: order.id,
modal: {
ondismiss: function () {
setIsLoading(false)
},
},
handler: async function (response) {
const data = await fetch("/api/order/verify", {
method: "POST",
body: JSON.stringify({
razorpayPaymentId: response.razorpay_payment_id,
razorpayOrderId: response.razorpay_order_id,
razorpaySignature: response.razorpay_signature,
email: userData.user?.email,
}),
})
const res = await data.json()
if (res?.error === false) {
// redirect to success page
router.push("/success")
}
},
prefill: {
email: userData.user?.email,
},
}
const paymentObject = new window.Razorpay(options)
paymentObject.open()
paymentObject.on("payment.failed", function (response) {
alert("Payment failed. Please try again.")
setIsLoading(false)
})
}
return (
<>
<Suspense fallback={<Loading />}>
<div className="">
<Button
className={cn(buttonVariants({ size: "lg" }))}
disabled={isLoading}
onClick={makePayment()}
>
Pay Now
</Button>
</div>
</Suspense>
</>
)
}
export default PaymentButton
That's itt!