How do you expose razorpay.open() and razorpay.close() without polluting the merchant's global namespace
Wrap everything in an IIFE/module so internals stay private via closure; expose exactly ONE namespaced, frozen global (window.Razorpay) whose methods are the public API. Guard against double-injection, avoid prototype pollution, and treat the global's shape as a stable contract.
The goal: the merchant's window gets exactly one new property, that property's internals are inaccessible, and the public surface is a deliberate, stable API.
The pattern: closure + a single namespaced global
Everything lives inside an IIFE (or an ES module bundled to one) so all internal variables, helpers, and state are private via closure — nothing leaks to window:
(function () {
// --- all private: closure-scoped, invisible to the host page ---
let iframe = null;
let isOpen = false;
function injectIframe(config) { /* ... */ }
function teardown() { /* ... */ }
// --- the ONE public global ---
const Razorpay = {
open(config) {
if (isOpen) return; // guard
iframe = injectIframe(config);
isOpen = true;
},
close() {
if (!isOpen) return;
teardown();
isOpen = false;
},
};
// guard against the script being included twice
if (!window.Razorpay) {
window.Razorpay = Object.freeze(Razorpay); // freeze: merchant can't tamper
}
})();The principles
- One global, namespaced. Not
window.open/window.close(collision with built-ins!) — one object,window.Razorpay, andopen/closeare methods on it. The whole API hangs off a single property.
- Internals private via closure.
iframe,isOpen, every helper — all closure-scoped inside the IIFE. The merchant's code literally cannot reach them. No internal state onwindow.
- Freeze the public object.
Object.freezeso the merchant (or a malicious script on their page) can't overwriteopen/closeor add properties. The contract is immutable.
- Guard against double-injection. If two
<script>tags load it, don't clobber an existing instance or run setup twice — checkif (!window.Razorpay).
- Don't touch shared prototypes. Never modify
Object.prototype,Array.prototype, etc. — that's the worst kind of pollution and would break the host page.
- Treat the global's shape as a public contract. Once merchants depend on
Razorpay.open(), its signature and behavior must stay backwards-compatible — version behavior internally, not the API shape.
Why it matters
You're a guest. Polluting window risks name collisions (you overwrite something the merchant uses, or vice versa), security leaks (internal state readable/tamperable), and debugging nightmares for the merchant. One frozen, namespaced global is the minimal, safe footprint.
The framing
"I wrap the whole thing in an IIFE so every internal — the iframe reference, open state, helpers — is private via closure and never touches window. Then I expose exactly one namespaced global, window.Razorpay, with open/close as methods on it — never bare globals that could collide with built-ins. I Object.freeze it so the host can't tamper with the API, guard against double-injection, and never touch shared prototypes. The merchant's window gains one immutable property; everything else is sealed in the closure. And that one global's shape becomes a stable public contract."
Follow-up questions
- •Why expose open/close as methods on one object instead of two globals?
- •How does the closure keep internal state private?
- •Why freeze the public object?
- •How do you handle the script being included twice?
Common mistakes
- •Adding multiple globals, or names that collide with built-ins (open, close, config).
- •Leaking internal state onto window.
- •Not freezing the API, letting the host overwrite methods.
- •Modifying shared prototypes.
- •No guard against double-injection.
Performance considerations
- •Negligible runtime cost — this is about footprint and safety, not speed. Keeping the public surface minimal also keeps the loader script small.
Edge cases
- •Two script tags loading the widget.
- •Merchant already has a global with the same name.
- •Malicious host script trying to overwrite Razorpay.open.
- •Merchant code reading/mutating what they think is internal state.
Real-world examples
- •Razorpay, Stripe, Intercom, Google Analytics — all expose a single namespaced global from an IIFE.
- •Any third-party embed script that must coexist with arbitrary host pages.