Frontend / web developer interview questions
Questions an interviewer can ask a Frontend / web developer, grouped by years of experience. Open one to see the strongest answer and why it works.
48 of the 337 questions our bank holds for this job. The rest are in the app.
By subject
- Web Fundamentals
- HTML
- CSS
- JavaScript
- TypeScript
- React
- Angular
- Vue
- Testing
- Performance
- Accessibility
- Tooling
- Security
0–1 years
They ask why 0 == '' is true and why you would not write that check. Strongest explanation?
The strongest answer: Say == coerces types; use === (or Object.is) and treat empty string and zero as separate cases
Why it works: Naming coercion and choosing === is the junior bar; then handle 0 and '' explicitly if both are valid.
- Explain coercion, then refuse it in app code.
- == turns values into a common type first.
- === compares type and value.
- If 0 and '' are both legal, check them by name.
The interviewer asks: “When do you use GET versus POST?” Strongest answer?
The strongest answer: GET for reads that must not change server state — it can be cached, logged and retried. POST when the call creates something or has a side effect.
Why it works: Exactly. The verb is a contract: GET is safe and idempotent; POST is allowed to create or change. Browsers, caches and CDNs rely on that.
- GET = no side effects; POST = create or change.
- Treat GET as cacheable and retry-safe.
- Use POST (or PUT/PATCH/DELETE) when the server will mutate.
- Never put secrets in a query string.
A form’s submit button sits under the home indicator and the keyboard. What is wrong?
The strongest answer: The layout ignores safe area and keyboard insets — I inset the content and keep the primary action visible while typing
Why it works: Exactly. Safe area and keyboard avoidance are part of shipping a phone UI — web or native.
- Respect safe areas.
- Avoid the keyboard; do not cover the action.
- Do not shrink type to cheat the layout.
- This is the same judgement as a responsive web form.
What is JSX, and what does the browser actually receive?
The strongest answer: A syntax for describing UI that compiles to function calls creating element objects; the browser never sees JSX — a build step turns it into plain JavaScript
Why it works: Right. Sugar over createElement; the compiler does the translation.
- JSX compiles to element-creating calls.
- Expressions in braces are JavaScript.
- className, not class.
- A build step is required.
You need to hide a panel but keep its space in the layout. What do you use, and why not display: none?
The strongest answer: visibility: hidden keeps the box; display: none removes it from layout and the accessibility tree
Why it works: visibility: hidden (or a sized inert region) is the direct answer; display: none is a different job.
- Pick the hide that matches layout and AT.
- display: none — gone from layout and usually from AT.
- visibility: hidden — space remains, not shown.
- If you use opacity, also remove it from focus (inert / aria-hidden + tabindex).
A list renders blank. The API returns `displayName`; the UI reads `name`.
What is the real fix?
The strongest answer: Agree the JSON contract — name, type and nullability — then change both sides to match it
Why it works: Exactly. A blank screen is a broken contract, not a CSS problem. Name, type and nullability are the join.
- The JSON is the join — agree it, then implement it twice.
- Write down name, type and whether it can be null.
- Change API and UI to that document, not to each other ad hoc.
- One unofficial alias is already a second contract.
What is the difference between props and state?
The strongest answer: Props are inputs passed from a parent and read-only in the child; state is data a component owns and changes over time, and changing it re-renders the component
Why it works: Right. Owned and mutable versus given and read-only.
- Props: passed in, read-only.
- State: owned, changed with a setter.
- Setting state re-renders.
- Lift state up when siblings share it.
A nav of links and a cart of line items — which HTML lists, and why not only divs?
The strongest answer: Use nav plus a list for the links, and ul/ol for the cart, so AT can announce counts and jump by list
Why it works: Lists are not decoration: they give count, list shortcuts, and a stable structure.
- Lists are for listed things.
- Nav links: nav wrapping a list.
- Cart rows: ul/ol (or a real data table if it is tabular).
- Don't fake layout with tables or bare spans.
The interviewer asks: “What's the difference between HTTP 401 and 403?” Strongest answer?
The strongest answer: 401: we don't know who you are — authenticate. 403: we know who you are, and you still may not do this.
Why it works: Exactly. 401 is missing or invalid credentials. 403 is an authenticated caller without permission. Clients branch: sign in vs you can't.
- 401 = who are you; 403 = you may not.
- 401: authenticate or refresh the token.
- 403: identity is known; authorization failed.
- Don't wrap either in HTTP 200.
A web teammate asks you to “just shrink the website” into the app WebView. What do you say?
The strongest answer: A phone has different input, density, offline, and platform chrome — we adapt layout and touch targets; we do not pinch a desktop page and call it an app
Why it works: Exactly. Responsive is not a scaled desktop. Touch, safe areas, and offline still apply.
- Design for the handset, not a scaled desktop.
- Touch targets and safe areas are required.
- WebView can host a flow; it is not a substitute for platform basics.
- Same judgement a frontend engineer should already have.
How do you commit a CSS fix that accidentally includes a .env file in the working tree?
The strongest answer: Leave the secret out of the commit, add .env to gitignore, and rotate the key if it was ever committed
Why it works: Secrets do not belong in git. Unstage, ignore, rotate if they leaked.
- Never commit secrets.
- Unstage .env; gitignore it.
- Commit only the CSS.
- If it was pushed, rotate the key — history rewrite is not enough.
Why do we serve every page over HTTPS and not just the login form?
The strongest answer: Because it protects integrity as well as secrecy: on plain HTTP anyone on the path can read or alter the page and inject script, and the session cookie travels on every request, not only on login
Why it works: Right — confidentiality, integrity and authenticity on every request. A page that can be rewritten in transit can steal the session on the next one.
- HTTPS is integrity, not just secrecy.
- Anything on HTTP can be rewritten.
- Sessions travel on every request.
- Redirect HTTP and set HSTS.
1–3 years
In-app navigation full-reloads the page. What did the markup do wrong?
The strongest answer: Use the router's Link for internal routes — a plain <a href> without the router does a full load
Why it works: Client routing is a Link (or useNavigate) that prevents the browser load. Anchors still matter for new tabs and progressive enhancement when the router handles them.
- Internal moves use the router.
- Link (or the framework equivalent) for in-app paths.
- Real <a> for external URLs.
- Don't assign location to move inside the SPA.
A setInterval inside useEffect always logs the initial count. Why?
The strongest answer: The effect's callback closed over the count from the render it was created in; either list count as a dependency and reset the interval, use a functional state update, or keep the latest value in a ref
Why it works: Right. Stale closure — the classic hooks trap.
- Closures capture the render's values.
- Dependencies re-create the effect.
- Functional updates avoid the capture.
- Refs hold the latest value.
A member must not see 'Transfer ownership'. An admin must.
What do you implement?
The strongest answer: Hide or disable the action for UX, and return 403 from the API with a code the UI already understands
Why it works: Exactly. Both. The hide prevents confusion; the 403 prevents curl. A shared error code keeps the message kind.
- UX hide + server 403.
- Same machine code on both.
- 'Internal' is not authz.
- Do not launder denies as 200.
When is useMemo worth it?
The strongest answer: When a computation is measurably expensive and its inputs change rarely, or when a stable reference is needed for a dependency or a memoised child; memoising cheap values costs more than it saves
Why it works: Right. Measure, then memoise the expensive and the reference-sensitive.
- Memoise expensive computations.
- Stable references for deps and memo children.
- Not by default.
- Profile first.
A useEffect fetches on mount and when userId changes, but the UI sometimes shows the previous user's data. Strongest approach?
The strongest answer: Put userId in the dependency array and abort or ignore stale responses in cleanup
Why it works: Deps plus cancellation (AbortController or an ignore flag) is the complete answer.
- Effects must match their data.
- List every value the effect reads.
- Cancel or ignore the in-flight request on change/unmount.
- Don't disable the exhaustive-deps rule to hide it.
Why wrap a handler in useCallback?
The strongest answer: To keep the same function reference between renders when it is passed to a memoised child or listed as an effect dependency; without that, the child re-renders or the effect re-runs every time
Why it works: Right. Reference stability for memo and dependencies.
- Stable reference across renders.
- Pairs with React.memo children.
- Dependencies must be listed.
- Skip it where nothing depends on identity.
A sortable list uses key={index} and inputs lose values when you reorder. Why, and what instead?
The strongest answer: Use a stable id as the key so React reconciles the same child; index keys break on insert and reorder
Why it works: Keys are identity. Index identity moves with position, so state follows the wrong row.
- Keys are identity, not position.
- Give each row a stable id.
- Don't use index for sortable or insertable lists.
- Never randomize keys.
A feed can be tens of thousands of items. How do you load it?
The strongest answer: Page or cursor-load, virtualize the window, and cancel in-flight work for rows that scrolled away — do not fetch the world into an array
Why it works: Exactly. Window the data and the views. Prefetch a little; do not ingest the corpus.
- Cursor / page the API.
- Virtualize the viewport.
- Cancel work for offscreen rows.
- Same idea as a virtualized web list.
When does React.memo help, and when does it not?
The strongest answer: It skips re-rendering a component when its props are shallowly equal — helpful for expensive components receiving stable props; useless when a prop is a new object or function every render, or when the component is cheap
Why it works: Right. Shallow prop comparison; new references defeat it.
- memo skips renders on equal props.
- Shallow comparison.
- Stable props or it is wasted.
- Expensive components only.
The register API can fail because the email is taken or because the password is too short.
What should the UI switch on?
The strongest answer: A stable machine code (`email_taken`) plus an optional message — not a translated sentence and not the HTTP status alone
Why it works: Exactly. Status is the class; the code is the case. Strings change with locale and copy tweaks.
- Machine code + HTTP class + safe message.
- UI switches on the code.
- Copywriters may change the sentence tomorrow.
- Do not parse exceptions or English.
3–5 years
The app feels sluggish when typing in one field. How do you find the cause?
The strongest answer: The React Profiler: record while typing, find which components re-render per keystroke and why, then fix the cause — state held too high, unstable props, an expensive child — rather than sprinkling memo
Why it works: Right. Measure, find the widest re-render, fix its cause.
- Profiler shows what rendered and why.
- State placement is the usual cause.
- Fix the source, then memo where needed.
- Verify with a second recording.
A teammate throws inside `fetch` so the React error boundary can show the outage.
What is the difference?
The strongest answer: An error boundary catches render throws; a failed fetch is data you must handle in the request path — do not throw in the fetch to 'reuse' the boundary for an API 500
Why it works: Exactly. Boundaries are for unexpected render failures. A 500 is an expected state of a request. Mixing them turns every outage into a white screen.
- Boundary = render/unknown.
- Fetch = loading/error/data.
- Do not throw on HTTP to be 'clever'.
- Keep the rest of the page alive.
A teammate wraps every component in React.memo. How do you respond?
The strongest answer: Memoize when profiling shows wasted renders and props stay stable — memo has a comparison cost and can hide stale bugs
Why it works: memo is a tool you apply after a profile, not a house style.
- Measure, then memoize.
- Profile the wasted renders.
- Stabilize props or hoist them.
- Don't wrap the tree by default.
A burst of autosaves returns 429 from the API gateway.
What does the UI do with 429?
The strongest answer: Back off using `Retry-After` (or a jittered default), tell the user we are slowing down, and do not tight-loop the same write
Why it works: Exactly. 429 is a contract: wait. The header is the join between gateway and spinner. A tight loop is how you DDoS yourself.
- Honour `Retry-After`.
- Jitter if the header is missing.
- Explain the wait; do not login-bounce.
- Never tight-loop a 429.
A designer’s card truncates at the largest Dynamic Type / font-scale. What is the bar?
The strongest answer: The task still works: wrap or reflow, do not clip essential text, and do not require a horizontal pan to read a price — I test the largest size we claim
Why it works: Exactly. Largest type is a supported configuration. Reflow beats pixel-perfect Figma.
- Reflow; do not clip the task.
- Test the largest font scale.
- Do not lock type to match a mock.
- Same bar as a responsive web page.
Where should state live, as a rule?
The strongest answer: As close as possible to where it is used — colocated — and lifted only as far as the components that need it; state high in the tree makes every change re-render everything beneath
Why it works: Right. Colocate; lift on demand.
- Colocate state.
- Lift only to the common owner.
- High state = wide re-renders.
- Global for cross-cutting data only.
Why do teams use a server-state library instead of useEffect plus useState for data?
The strongest answer: Because caching, deduplication, background refetch, stale-while-revalidate, retries, pagination and invalidation are the actual problem, and each hand-rolled effect solves a slice of it badly; server state is a cache, and the library treats it as one
Why it works: Right. Server state is a cache with a lifecycle; treat it as one.
- Server state = cache.
- Dedupe, refetch, invalidate.
- Loading and error handled once.
- Effects for local synchronisation.
Three apps disagree on 'primary' blue. How do you fix it as a system?
The strongest answer: One token source — Style Dictionary or CSS variables — consumed by web and native; no raw hex in features
Why it works: A token pipeline is how brands stay one product. Features consume names, not paint.
- Tokens are the contract.
- Author color once.
- Emit CSS variables and native tokens.
- Ban hex in feature code.
A checkout form has conditional sections, a confirm-email field that must match, and rules that change with the delivery country.
Template-driven or reactive forms, and where do the rules live?
The strongest answer: Reactive forms: the model and validators live in the class, the match rule is a group-level validator, country changes swap validators in code, and all of it is unit-testable without the DOM
Why it works: Exactly — complex, dynamic rules belong in an explicit form model. A validator on the group sees both fields, and typed controls catch mistakes early.
- Dynamic rules need a form model.
- Reactive forms for complex validation.
- Cross-field rules: validator on the group.
- Test validators without rendering.
Growth wants a browser push prompt on the homepage so that more visitors opt in.
What do you say, and when should the prompt appear?
The strongest answer: Ask in context, right after an action that implies wanting updates, and explain why in our own UI first — the browser prompt is one shot per site and a refusal is effectively permanent
Why it works: Right — a pre-prompt you control can be asked again, the native one cannot, and browsers also quieten prompts that are not tied to a user action.
- Earn the ask before you spend it.
- Your own pre-prompt first.
- Trigger on a relevant user action.
- A block is effectively permanent.
A “perfectly virtualized” list still drops frames on mid phones. What do you inspect next?
The strongest answer: The size of the overscan window, span counts, placeholder vs real bind cost, and whether I am measuring every row on the UI thread — virtualization is necessary, not sufficient
Why it works: Exactly. Window, span, and measure cost are the next knobs. Profile them.
- Profile overscan and measure.
- Keep placeholders cheaper than the real row.
- Watch span / grid costs.
- Do not “fix” frames by changing the store device floor first.
A cross-platform codebase has is-iOS / is-Android checks scattered through the screens, and a fix for one platform keeps breaking the other. How do you organise it?
The strongest answer: Put the difference behind one interface the shared code calls, with the platform implementations at the edge, so a screen reads the same on both and each platform has exactly one place to change
Why it works: Exactly — shared code stays platform-blind, and the platform detail lives where the platform team can own it.
- Hide platform detail behind an interface.
- Shared code stays platform-blind.
- Implementations live at the edge.
- Never fork the screen.
5+ years
Product wants full click tapes plus form field values in the frontend logger. How do you respond?
The strongest answer: Minimize: no secrets or PII in RUM, scrub fields, purpose-limit the log, and run it past privacy
Why it works: Frontend logs are production data. Minimization and purpose are the staff answer, not a bigger tape.
- Collect less than product asked.
- Scrub inputs and secrets.
- State the purpose.
- Never log passwords, even temporarily.
Choosing between client rendering, server rendering, static generation and streaming for a new product — how do you decide?
The strongest answer: By page: static for content that rarely changes, server or streaming for personalised first paint and SEO, client rendering for logged-in tools where interactivity matters more than first paint; most products mix them per route
Why it works: Right. Per route, by what the page needs.
- Static for stable content.
- Server or streaming for personalised first paint.
- Client for authenticated tools.
- Mix per route.
A 400-component app has become impossible to change safely. What structural move do you make first?
The strongest answer: Draw feature boundaries: group by feature with public entry points, forbid cross-feature imports of internals with lint rules, and pull truly shared pieces into a design system and a small core — before any rewrite
Why it works: Right. Boundaries and enforced imports make change local again.
- Feature folders with public APIs.
- Lint rules on imports.
- Shared core kept small.
- Refactor before rewrite.
Three product teams ship the same header with three bugs. How do you organize frontend platform work?
The strongest answer: A platform module with a clear API, versioning, and owners — products consume it and do not fork it
Why it works: Platform is an owned product: interface, release train, support. Snippets are how bugs multiply.
- Treat shared UI as a product.
- One owned package and API.
- Version and changelog.
- Products depend; they don't fork.
A critical vulnerability is published in a package three levels down your dependency tree. The scanner is red on every frontend repository.
What is your process?
The strongest answer: Check whether the vulnerable code is reachable in what we ship, upgrade the direct dependency that pulls it in, use a dated override only if no release fixes it, and verify via lockfile and CI
Why it works: Right — triage by exposure, fix at the direct dependency, keep any override temporary and visible, and let the lockfile prove what is installed.
- Triage, fix at the source, verify.
- Is the vulnerable code reachable?
- Upgrade the direct parent dependency.
- Overrides are dated stopgaps.
Different screens show different values for the same entity. What is the root cause and the design fix?
The strongest answer: Multiple copies of server state in different caches and local states; one normalised server-state cache keyed by entity id, with mutations invalidating or updating it, so every screen reads one truth
Why it works: Right. One cache, one identity per entity.
- One server-state cache.
- Normalise by id.
- Mutations update the cache.
- Screens read, never copy.
Legal says marketing pixels cannot run before consent. How do you architect the frontend?
The strongest answer: A CMP gate: load tags only after purpose consent, honor region and withdrawal, and set no marketing cookies before that
Why it works: Consent is a load-order problem. Pixels are code; they do not run until the purpose is granted.
- Consent before tags.
- CMP as the gate.
- Purpose IDs, not one boolean.
- Withdrawal unloads tags and clears that purpose.
Performance regresses every quarter despite fixes. How do you make it stick?
The strongest answer: Budgets enforced in CI, real-user metrics on a dashboard with owners, a performance review in the definition of done for new surfaces, and a habit of measuring before optimising — culture and tooling, not a sprint
Why it works: Right. Enforcement and ownership outlast sprints.
- Budgets in CI.
- Real-user metrics with owners.
- Performance in the definition of done.
- Measure before optimising.
A junior's PR works but has an XSS hole and no tests. How do you mentor in review?
The strongest answer: Block the vuln, explain the path, pair on a test, and don't shame them
Why it works: Security is non-negotiable; the lesson is the point of seniority. Pairing beats a drive-by fix.
- Protect users, then teach.
- Do not merge the hole.
- Walk the failure mode.
- Write the test together.
Your OpenAPI document lists every path and method and nothing else. A partner team says integrating still took them a week.
What is missing?
The strongest answer: What they actually integrate against: request and response schemas, the error bodies and status codes, the auth scheme, pagination and examples — plus a CI check that the document still matches the running code
Why it works: Right — and the CI check is the half most teams skip; a spec nobody verifies becomes fiction within a quarter.
- The spec is the contract.
- Schemas, errors, auth, pagination.
- Ship examples, not only paths.
- Check the spec against the code in CI.
'1 files' and Hebrew plural forms are wrong. What does the i18n layer need?
The strongest answer: ICU/CLDR plural categories per locale — not an English-only count === 1 check
Why it works: Pluralization is a locale table, not a boolean. Hebrew, Russian, and Arabic are the usual interview examples.
- Use real plural rules.
- ICU messages, not if (count === 1).
- Translators fill each category.
- Don't hide the number.
A trader UI keeps the whole order book in a WebSocket reducer. A blip shows an empty book.
What is the socket allowed to be?
The strongest answer: A hint to refetch or a delta on top of a REST/GraphQL snapshot — if the socket drops, the UI can still rebuild state from HTTP
Why it works: Exactly. Sockets die. Mobile radios die. The snapshot is the source of truth; the socket is freshness. Otherwise every reconnect is a product bug.
- HTTP (or a query) can rebuild the world.
- The socket pushes invalidations or deltas.
- Reconnect = snapshot, then resume.
- Do not hide authz in a message handler only.