Razorpay Frontend Interview Questions
Payments. JavaScript depth and a machine coding round.
Razorpay builds payment products for businesses: the checkout that runs inside a merchant's website, the dashboard merchants manage money from, and banking and payroll tools around them. Frontend code here handles money and often runs on pages Razorpay does not control, so interviewers care about correctness, security and what happens when the network fails.
Candidates commonly describe a loop built on core JavaScript and a hands-on build round, with framework questions second. The questions below follow that shape.
- Questions
- 15, 5 with free answers
- Interview rounds
- 5
- Last reviewed
- October 2026
Razorpay frontend interview process
The usual shape of the loop. A guide to what to prepare, not a promise of what you will get.
- 1
Screening
A call or online round on JavaScript fundamentals: closures,
this, promises and output-based snippets. - 2
Machine coding
About 60 to 90 minutes building a small UI, such as a form flow or a search component. Some interviewers ask for plain JavaScript.
- 3
Frontend deep dive
Browser internals, async patterns, React rendering, security and performance, usually tied to your past work.
- 4
Frontend system design
More common for SDE 2 and above: design a widget or a dashboard end to end.
- 5
Hiring manager
Past projects, ownership and how you work with other teams.
What interviewers tend to look for
- Writing utilities such as debounce, a promise polyfill or a deep clone from scratch.
- Handling failure: retries, timeouts and a payment form submitted twice.
- Security basics: XSS, CSRF and where tokens are stored.
- Working code inside the time limit, before polish.
Your Razorpay mock interview
15 questions. Type an answer under each one and an AI reviewer scores it and tells you what you missed. Stuck on one? Ask for a hint. A free account gets 3 AI replies a day.
Your mock interview
0 of 15 answered
Answer a question and your score shows up here.
JavaScript
6 questions- Question 1 of 15JavaScript
Razorpay interviewer
What is a closure? Give a practical use for one.
Model answer
A closure is a function together with the scope it was created in. The function keeps access to the variables of that scope even after the outer function has returned.
function createCounter() { let count = 0; // private: nothing outside can touch it return { increment: () => ++count, value: () => count, }; } const counter = createCounter(); counter.increment(); counter.value(); // 1Practical uses: private state as above, function factories, and utilities such as
debounceandmemoize, which keep a timer or a cache between calls.The cost to mention: a closure keeps its captured variables alive for as long as the function is reachable, so a long-lived callback holding a large object is a memory leak.
- Question 2 of 15JavaScript
Razorpay interviewer
Explain the event loop. In what order do a setTimeout, a promise callback and synchronous logs run?
Model answer
JavaScript runs on one call stack. When the stack is empty, the event loop first runs every queued microtask (promise callbacks,
queueMicrotask,MutationObserver), then takes one macrotask (a timer, an I/O callback, a UI event), and the browser may render in between.console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D'); // A, D, C, BSynchronous code finishes first (A, D). The promise callback is a microtask, so it runs before the timer, which is a macrotask, even with a 0 ms delay.
With
asyncfunctions, everything after anawaitis scheduled as a microtask. A common follow-up is a longer snippet mixing both; work through it queue by queue instead of guessing. - Question 3 of 15JavaScript
Razorpay interviewer
Implement Promise.all.
Model answer
function promiseAll(iterable) { return new Promise((resolve, reject) => { const items = Array.from(iterable); const results = new Array(items.length); let pending = items.length; if (pending === 0) return resolve(results); items.forEach((item, index) => { Promise.resolve(item).then((value) => { results[index] = value; // by index, not by finish order pending -= 1; if (pending === 0) resolve(results); }, reject); }); }); }What the interviewer checks:
- Results keep the input order, whichever promise finishes first.
- Plain values are accepted, which is what
Promise.resolve(item)handles. - The first rejection rejects the whole thing.
- An empty input resolves straight away with an empty array.
Expect a follow-up asking for
Promise.allSettled,Promise.anyorPromise.race. - Question 4 of 15JavaScript
Razorpay interviewer
What is the difference between debounce and throttle? Implement debounce.
Model answer
Debounce waits until the calls stop for a set time, then runs once. Use it for a search box or for saving after the user stops typing. Throttle runs at most once per interval while the calls keep coming. Use it for scroll, resize and drag handlers.
function debounce(fn, wait) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; }Common follow-ups: keep
thisand the arguments (done above), add acancelmethod, add aleadingoption that fires on the first call, and then write throttle. - Question 5 of 15JavaScript
Razorpay interviewer
Write a polyfill for Function.prototype.bind.
Model answer
Function.prototype.myBind = function (context, ...preset) { const fn = this; return function bound(...args) { // Called with `new`: the bound `this` is ignored. if (new.target) return new fn(...preset, ...args); return fn.apply(context, [...preset, ...args]); }; }; function greet(greeting, name) { return greeting + ', ' + name + ' from ' + this.city; } const hello = greet.myBind({ city: 'Pune' }, 'Hello'); hello('Asha'); // "Hello, Asha from Pune"Three things to cover: it returns a new function, it supports partial application (the preset arguments come first), and a bound function used with
newignores the boundthis. Most candidates miss the third. - Question 6 of 15JavaScript
Razorpay interviewer
What is the difference between a shallow and a deep copy? How would you deep clone an object?
Model answer
A shallow copy (
{ ...obj },Object.assign,arr.slice()) copies one level. Nested objects are still shared with the original.structuredClone(value)is the built-in deep clone. It handles dates, maps, sets, typed arrays and circular references. It throws on functions and DOM nodes and does not keep the prototype chain.JSON.parse(JSON.stringify(value))is weaker: it dropsundefinedand functions, turns dates into strings and fails on cycles.Interviewers usually want it written by hand:
function deepClone(value, seen = new WeakMap()) { if (value === null || typeof value !== 'object') return value; if (seen.has(value)) return seen.get(value); // circular reference const copy = Array.isArray(value) ? [] : {}; seen.set(value, copy); for (const key of Reflect.ownKeys(value)) { copy[key] = deepClone(value[key], seen); } return copy; }Say what it does not cover (Date, Map, Set, class instances) and how you would add each one.
TypeScript
1 question- Question 7 of 15TypeScript
Razorpay interviewer
What is type narrowing? What is a discriminated union?
Model answer
Narrowing is TypeScript refining a broad type inside a branch, based on a check it understands:
typeof,instanceof,in, equality, truthiness, or a user-defined guard that returnsvalue is T.A discriminated union is a union of object types that share one literal field. Checking that field narrows to the right member:
type RequestState = | { status: 'loading' } | { status: 'success'; data: string[] } | { status: 'error'; message: string }; function render(state: RequestState) { switch (state.status) { case 'loading': return 'Loading'; case 'success': return state.data.join(', '); // data exists here case 'error': return state.message; // message exists here } }The reason to prefer this over one object with optional
dataanderrorfields: impossible combinations, such as loading with an error message, cannot be written at all.
React
3 questions- Question 8 of 15React
Razorpay interviewer
What makes a component re-render, and how do you cut unnecessary re-renders?
Model answer
A component re-renders when its own state changes, when its parent re-renders, or when a context it reads changes. A changed prop is not a separate cause: props change because the parent rendered.
A re-render is React calling the function again. It is not a DOM update, and most are cheap. Measure with the React DevTools Profiler before changing anything.
When one is a real problem, in rough order of preference:
- Move state down into the component that uses it, so fewer components sit below it.
- Pass expensive content as
children. Elements created by a parent that did not re-render are reused as they are. - Wrap a costly child in
React.memoand give it stable props. - Split a large context, or use a store that lets components subscribe to a slice.
- Virtualise long lists.
- Question 9 of 15React
Razorpay interviewer
How does useEffect work? Explain the dependency array, cleanup and the common mistakes.
Model answer
An effect runs after React has committed a render. Its job is to keep something outside React in step with your state: a subscription, a timer, a DOM API or a network request.
- No dependency array: runs after every render.
- An empty array: runs once after the first render.
- With dependencies: runs again when any of them changes, compared with
Object.is. - The function you return is the cleanup. It runs before the next run of the effect and when the component unmounts.
useEffect(() => { const controller = new AbortController(); fetch('/api/users/' + id, { signal: controller.signal }) .then((response) => response.json()) .then(setUser) .catch((error) => { if (error.name !== 'AbortError') setError(error); }); return () => controller.abort(); }, [id]);Mistakes interviewers look for: leaving a dependency out and reading a stale value, depending on an object or function that is recreated every render (an endless loop), not handling a response that arrives after
idhas changed, and using an effect to compute something from props that could be computed during render.In development, Strict Mode mounts each component, unmounts it and mounts it again, to expose effects that do not clean up.
- Question 10 of 15React
Razorpay interviewer
What is a custom hook? Write useDebounce.
Model answer
A custom hook is a function whose name starts with
useand which calls other hooks. It lets several components share stateful logic. It shares the logic, not the state: every component that calls the hook gets its own copy.function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; } function Search() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query); useEffect(() => { if (debouncedQuery) search(debouncedQuery); }, [debouncedQuery]); }Others that come up often:
usePrevious,useLocalStorage,useFetch,useOnClickOutsideanduseIntersectionObserver. For each, name the cleanup and what happens on the server, wherewindowdoes not exist.
Next.js
1 question- Question 11 of 15Next.js
Razorpay interviewer
What is middleware in Next.js, and what should it not be used for?
Model answer
Middleware is a function that runs before a request reaches a route. It can redirect, rewrite to a different path, or change headers and cookies. Next.js 16 renames the file convention to
proxy; the idea is the same.Good uses: sending signed-out users to the login page, redirects for old URLs, choosing a locale, A/B test bucketing, and adding security headers.
import { NextResponse } from 'next/server'; export function middleware(request) { const signedIn = request.cookies.has('session'); if (!signedIn && request.nextUrl.pathname.startsWith('/account')) { return NextResponse.redirect(new URL('/login', request.url)); } } export const config = { matcher: ['/account/:path*'] };What to avoid:
- Slow work. It runs on every matched request, so a database query here slows the whole site. Use
matcherto limit where it runs. - Treating it as the only authorisation check. Check again where the data is read, because a route or action can be reached in ways the middleware does not cover.
- Slow work. It runs on every matched request, so a database query here slows the whole site. Use
CSS
1 question- Question 12 of 15CSS
Razorpay interviewer
How does the browser decide which CSS rule wins?
Model answer
When several declarations target the same property on the same element, the cascade checks, in order:
- Origin and importance:
!importantdeclarations beat normal ones. - Cascade layers: for normal declarations, a later
@layerbeats an earlier one, and styles outside any layer beat all layers. - Specificity.
- Source order: the last one written wins.
Specificity is compared as three numbers: ids, then classes, attributes and pseudo-classes, then element names and pseudo-elements.
#nav .linkis (1,1,0) and beats.menu .item .linkat (0,3,0), because ids are compared first. An inlinestyleattribute beats any selector.Useful details:
:where()always has zero specificity, while:is()and:not()take the specificity of their most specific argument.In practice, keep specificity low and flat by styling with single classes, and use
@layerto order resets, components and utilities instead of fighting with!important. - Origin and importance:
Browser, performance and system design
1 question- Question 13 of 15Browser, performance and system design
Razorpay interviewer
What are XSS and CSRF, and how do you prevent them?
Model answer
XSS (cross-site scripting): an attacker gets their script to run in your page, with your user's access. It can be stored (saved in a comment and shown to others), reflected (bounced back from a URL parameter) or DOM-based (your own JavaScript writes untrusted text into the page).
- Escape everything you render. React does this by default;
dangerouslySetInnerHTMLandinnerHTMLare the holes. - When you have to render user-supplied HTML, sanitise it with a library such as DOMPurify.
- Add a Content Security Policy that blocks inline scripts and unknown sources.
- Keep session cookies
HttpOnlyto limit the damage. - Do not put user input into
hreforsrcwithout checking the scheme, becausejavascript:URLs execute.
CSRF (cross-site request forgery): another site makes the user's browser send a request to yours, and the browser attaches their cookies.
- Set
SameSiteon cookies. - Require a CSRF token, or check the
Originheader, on requests that change data. - Never change data in response to a GET.
Also worth a sentence:
frame-ancestorsin the CSP stops clickjacking, and anything in the frontend bundle is public, so API secrets cannot live there. - Escape everything you render. React does this by default;
Machine coding
2 questions- Question 14 of 15Machine coding
Razorpay interviewer
Build an OTP input with one box per digit.
Model answer
State is an array of single characters, with an array of refs so that focus can be moved between the boxes.
function handleChange(index, raw) { const digit = raw.replace(/\D/g, '').slice(-1); if (!digit) return; const next = [...digits]; next[index] = digit; setDigits(next); if (index < length - 1) inputs.current[index + 1].focus(); if (next.every(Boolean)) onComplete(next.join('')); } function handleKeyDown(index, event) { if (event.key === 'Backspace') { event.preventDefault(); const next = [...digits]; if (next[index]) { next[index] = ''; } else if (index > 0) { next[index - 1] = ''; inputs.current[index - 1].focus(); } setDigits(next); } }The behaviours that are checked:
- Typing a digit moves to the next box. Backspace clears the box, or moves back if it is already empty.
- Pasting a full code fills all the boxes. Handle
onPasteon the container and spread the digits. - Only digits are accepted.
- Left and right arrows move between boxes.
inputMode="numeric"shows the number keypad on phones, andautoComplete="one-time-code"lets the browser offer the code from an SMS.- The first box is focused on mount, and
onCompletefires once all are filled.
- Question 15 of 15Machine coding
Razorpay interviewer
Build a multi-step form with validation.
Model answer
Keep one state object for the whole form and one number for the current step. Each step is a component that receives the data and a change handler. Keeping the data at the top is what lets Back and Next keep what was typed.
const STEPS = [ { title: 'Account', fields: ['email', 'password'] }, { title: 'Profile', fields: ['name', 'city'] }, { title: 'Review', fields: [] }, ]; function next() { const errors = validate(data, STEPS[step].fields); setErrors(errors); if (Object.keys(errors).length === 0) setStep((s) => s + 1); }- Validate only the current step's fields before moving on, and all of them again on submit.
- Show each error next to its field, tied to it with
aria-describedby, and setaria-invalid. Move focus to the first invalid field. - Back never validates and never loses data.
- A progress indicator showing the current step and how many there are.
- On submit, disable the button, show progress, and handle a failed request without clearing the form.
Follow-ups: steps that depend on earlier answers, saving a draft to
localStorage, describing the fields in a config so that the form is generated, and when you would use a form library and a schema validator instead.
Practise for Razorpay
Reading answers is the easy half. These problems run in the browser against real tests.
