Full-Stack Web Developer Interview Guide
213 in-depth full-stack web development interview questions with detailed easy-to-understand explanations, comparison tables, visual diagrams and practical code, covering HTML, CSS, JavaScript, TypeScript, React, Node.js, Express, REST APIs, databases, authentication, security, Git and Docker. Tick each question off as you master it.
HTML5 & Accessibility
How a browser builds a page from your HTML
The browser reads your HTML top to bottom, builds a tree of nodes called the DOM, merges it with the page's CSS rules, and finally paints it. Semantic tags and correct nesting matter because this whole pipeline depends on a well-formed tree.
001What is HTML5 and how is it different from older HTML?Beginner+
Easy explanation
HTML5 is the current version of HTML, the markup language browsers use to understand the structure of a web page. Compared to older versions like HTML4, it added meaningful (semantic) tags such as header, nav, main, article, section and footer, so the structure of a page is described by the tags themselves instead of generic div soup with class names.
It also brought in native support for audio and video without plugins like Flash, form input types like email and date, offline storage APIs, and a simpler doctype. In an interview, the strongest answer is not just 'it's the newest version' — it's explaining that HTML5 shifted the web toward meaning-carrying markup, which then powers accessibility and SEO for free.
| Aspect | HTML4 | HTML5 |
|---|---|---|
| Structure tags | Only div/span, no built-in meaning | header, nav, main, article, section, footer |
| Media | Needed Flash/plugins for video and audio | Native <video> and <audio> tags |
| Forms | Only text/checkbox/radio etc. | Adds email, date, number, range, color and built-in validation |
| Doctype | Long and version-specific | Simple: <!doctype html> |
| APIs | None built in | Canvas, Geolocation, Web Storage, Drag and Drop |
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Portfolio</title></head>
<body>
<header>Site header</header>
<main><article>My latest project</article></main>
<footer>© 2026</footer>
</body>
</html>
002What is semantic HTML and why does it actually matter?Beginner+
Easy explanation
Semantic HTML means picking a tag because of what it means, not just how it looks. header, nav, main, article, aside and footer tell the browser, screen readers and search engines what role each part of the page plays, the same way a table of contents tells a reader what a book chapter is about.
The practical payoff is threefold: screen readers can let a blind user jump straight to 'main content' or 'navigation' instead of reading every div; search engines can better understand which text is the real content versus a sidebar ad; and other developers reading your code understand the layout instantly without opening a class-name dictionary. A div with class='navbar' works visually but gives none of these benefits for free.
<header>
<h1>My Blog</h1>
<nav><a href="/">Home</a><a href="/about">About</a></nav>
</header>
<main>
<article><h2>Post title</h2><p>Post content...</p></article>
<aside>Related posts</aside>
</main>
<footer>Contact us</footer>
003div vs span — when do you use which?Beginner+
Easy explanation
div is a generic block-level container: it starts on a new line and takes the full available width, so it's used to group bigger chunks of content like a card, a form section or a whole sidebar.
span is a generic inline container: it doesn't break the line, so it's used to wrap a small piece of text inside a sentence, like highlighting one word or wrapping an icon next to a label. The rule of thumb interviewers want to hear: reach for div/span only when no semantic tag fits; otherwise prefer header/nav/main/etc. for blocks and strong/em/mark for inline meaning.
| div | span | |
|---|---|---|
| Display | block (own line, full width) | inline (flows with text) |
| Typical use | Grouping a section, card, or layout区块 | Styling part of a sentence or icon |
| Can contain | Other block or inline elements | Usually just inline content |
<div class="card">
Price: <span class="highlight">$49</span> per month
</div>
004What is the DOM and how does JavaScript use it?Intermediate+
Easy explanation
The DOM (Document Object Model) is the browser's live, in-memory tree representation of your HTML document. Every tag becomes a 'node' in this tree, and JavaScript can read, add, remove or change any of these nodes at any time — that's what makes a page interactive instead of a static printout.
Think of the HTML file as the blueprint and the DOM as the actual building the browser constructed from it. When you call document.querySelector, you're asking the browser to find a node in that building; when you set .textContent, you're physically changing that room. This distinction (source HTML vs live DOM) is a very common trick question — 'view source' shows the original file, but DevTools 'Elements' tab shows the current DOM, which JavaScript may have already changed.
const title = document.querySelector('h1');
title.textContent = 'Updated title';
console.log(title.parentElement); // the live DOM node's parent
005What does the viewport meta tag do and why is every mobile site missing without it?Beginner+
Easy explanation
Mobile browsers historically rendered pages assuming a wide desktop-like layout (around 980px) and then shrank everything to fit the phone screen, making text tiny and layouts useless. The viewport meta tag tells the browser 'use the actual device width as the layout width, and start at 1x zoom', which is what makes responsive CSS (media queries, flexible grids) actually work on a phone.
Without it, your carefully written mobile-first CSS breakpoints simply won't trigger correctly, because the browser is still pretending the screen is 980px wide. It's one line, but it is the single most common reason a 'responsive' site looks broken only on a real phone.
<meta name="viewport" content="width=device-width, initial-scale=1">
006Why is alt text important, and what should decorative images use?Beginner+
Easy explanation
The alt attribute gives a text alternative for an image: screen readers speak it aloud, and browsers show it if the image fails to load. Good alt text describes the meaning of the image in context — 'Revenue increased 24% in Q2' for a chart, not 'chart.png' or 'image123'.
Purely decorative images (a background flourish, a spacer) that add no information should use an empty alt="" so screen readers skip them entirely instead of announcing something meaningless like 'image, image, image' between real content. Getting this distinction right is a core accessibility (a11y) interview signal.
<!-- meaningful image -->
<img src="chart.png" alt="Revenue increased 24% in Q2">
<!-- decorative image -->
<img src="swirl.png" alt="">
007What is ARIA and when should you actually reach for it?Intermediate+
Easy explanation
ARIA (Accessible Rich Internet Applications) is a set of extra HTML attributes — like role, aria-expanded, aria-label, aria-live — that describe a UI's role, state or purpose to assistive technology when plain HTML can't express it, such as a custom dropdown built from divs.
The golden rule interviewers look for: 'No ARIA is better than bad ARIA.' Always prefer a native element first — a real <button> already has keyboard support, focus handling and a role built in for free. Only add ARIA when you've built a custom widget (like a tab panel or a custom toggle) that native HTML has no equivalent for, and even then you must also wire up the actual keyboard behavior yourself — ARIA only changes what's announced, not what happens.
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>
<!-- clicking toggles aria-expanded and the hidden attribute -->
008What is progressive enhancement, and how is it different from graceful degradation?Intermediate+
Easy explanation
Progressive enhancement means you build the simplest, fully-working version first — plain HTML that works even with CSS and JavaScript turned off — and then layer on CSS for style and JavaScript for extra interactivity as enhancements. If JavaScript fails to load on a flaky mobile connection, the core task (like submitting a search) still works because it was a real HTML form all along.
Graceful degradation is the opposite direction: you build the full rich experience first, then try to make sure it doesn't completely break on older or limited browsers. Progressive enhancement is generally considered the more robust, accessibility-friendly philosophy because the baseline is guaranteed to work rather than patched after the fact.
<form action="/search" method="get">
<input name="q">
<button>Search</button>
</form>
<!-- Works with plain HTML; JS can later intercept submit for an AJAX experience. -->
009What is native lazy loading for images and when should you avoid it?Intermediate+
Easy explanation
The loading="lazy" attribute tells the browser to skip downloading an image until it's about to scroll into view, which speeds up the initial page load, especially on long pages full of images like a product listing or a blog with photos.
You should NOT lazy-load the image that appears above the fold (immediately visible when the page opens), like a hero banner, because delaying it actually hurts perceived load speed and can hurt your Largest Contentful Paint (LCP) score. The rule: lazy-load what's below the fold, eager-load what's visible immediately.
<!-- below the fold: lazy is good -->
<img src="gallery-5.webp" loading="lazy" alt="Team offsite photo">
<!-- hero image: do NOT lazy load -->
<img src="hero.webp" loading="eager" alt="Product hero shot">
010async vs defer on script tags — what's the real difference?Intermediate+
Easy explanation
By default, a <script> tag blocks the HTML parser: the browser stops building the page, downloads the script, runs it, and only then continues parsing HTML. Both async and defer fix this by downloading the script in the background without blocking parsing, but they differ in when the script actually executes.
defer waits until the HTML document is fully parsed, and runs multiple deferred scripts in the order they appear in the document — this is what you want for most app scripts that need the full DOM and a predictable order. async runs the script the instant it finishes downloading, which could be before or after parsing finishes, and does not guarantee order between multiple async scripts — this is better suited for independent scripts like analytics that don't depend on the DOM or on each other.
| Normal <script> | async | defer | |
|---|---|---|---|
| Blocks HTML parsing while downloading? | Yes | No | No |
| Executes | Immediately (blocking) | As soon as it's downloaded, in any order | After parsing finishes, in document order |
| Best for | Nothing modern | Independent scripts like analytics | App logic depending on the DOM |
<script src="analytics.js" async></script>
<script src="app.js" defer></script>
011What are HTML forms best practices for validation?Intermediate+
Easy explanation
HTML5 gives you free client-side validation with attributes like required, type="email", minlength, pattern and min/max, which the browser checks before submitting and shows a native error bubble for — no JavaScript required for basic cases.
But client-side validation is only a usability nicety, never a security boundary — a user can disable JavaScript or send a request directly with curl, bypassing every HTML attribute. The real rule: validate on the client for a fast, friendly experience, but always re-validate everything on the server before trusting or storing it.
<form>
<input type="email" required>
<input type="password" minlength="8" required>
<button type="submit">Sign up</button>
</form>
012What is the difference between id and class attributes?Beginner+
Easy explanation
An id must be unique within the page — only one element can have a given id — and is used for things like linking to a page section, labelling a form field, or a single JavaScript hook. A class can be applied to many elements at once and is used for reusable styling or grouping, like every 'card' on a page.
In CSS specificity, an id selector (#header) outranks a class selector (.header) which is one reason many teams intentionally style almost everything with classes and reserve ids for JavaScript hooks or anchor links, keeping specificity predictable.
<h2 id="pricing">Pricing</h2> <!-- unique anchor target -->
<div class="card">Basic</div>
<div class="card">Pro</div> <!-- reusable class -->
013What is the difference between HTML entities and raw characters, and when do you need them?Beginner+
Easy explanation
Some characters have special meaning in HTML — < and > define tags, & starts an entity, and quotes delimit attributes — so if you want to literally display them as text, you must escape them using an entity like <, >, & or ".
This matters a lot in security discussions too: when a server renders user-submitted text back into HTML, failing to escape these characters is exactly how cross-site scripting (XSS) attacks get injected, because a raw < in the wrong place can open a real script tag instead of showing text.
<p>Use <div> for a block, not &div;</p>
<!-- renders as: Use <div> for a block, not ÷ -->
014How do you make a website accessible for keyboard-only users?Advanced+
Easy explanation
A meaningful share of users (motor impairments, power users, screen-reader users) never touch a mouse: they Tab between interactive elements and press Enter/Space to activate them. Every clickable thing must be reachable in a logical order, must show a visible focus outline, and must be operable with the keyboard — which native elements like button and a get for free, but a div styled to look like a button does not.
Common failures interviewers probe for: removing the focus outline with `outline: none` and not replacing it with an equally visible custom style, using a div with an onclick instead of a real button, and building custom dropdowns/modals without trapping focus inside them while they're open. Testing a page by only using the Tab and Enter keys is the fastest way to catch these bugs yourself.
/* Bad: removes focus visibility for everyone */
button:focus { outline: none; }
/* Good: keep it visible, just restyle it */
button:focus-visible { outline: 2px solid #4966a8; outline-offset: 2px; }
015What is the difference between HTML tables for layout vs for data, and why does it matter?Intermediate+
Easy explanation
In the early 2000s, developers commonly used <table> purely to arrange page layout (columns, sidebars) because CSS layout tools were weak. Modern CSS (Flexbox, Grid) replaced that use case entirely, so tables today should be reserved for actual tabular data — rows and columns of related data like a price comparison or a spreadsheet export.
The reason this still comes up in interviews: a table used for layout confuses screen readers, which announce row/column headers as if the content were real data, making the page harder to navigate for a blind user. If you see a layout table in a codebase today, it's a strong signal of an old or poorly-maintained project.
<!-- correct use of a table: real tabular data -->
<table>
<thead><tr><th>Plan</th><th>Price</th></tr></thead>
<tbody><tr><td>Basic</td><td>$9</td></tr></tbody>
</table>
016What is the difference between localStorage, sessionStorage and cookies?Advanced+
Easy explanation
All three let a browser store small pieces of data tied to a website, but they differ in lifetime and where they're visible. localStorage persists until explicitly cleared (even after closing the browser) and stays only on the client. sessionStorage is scoped to one browser tab and disappears when that tab closes. Cookies persist based on an expiry you set, are much smaller (about 4KB), and — critically — are automatically sent with every HTTP request to the server, which is why they're used for authentication sessions.
Because cookies travel with every request, they're the right tool when the server needs to know who's asking (like a login session, using HttpOnly and Secure flags). localStorage/sessionStorage are the right tool for pure client-side state the server never needs to see, like a draft form or a UI preference — but never store sensitive tokens in localStorage since any injected script (XSS) can read it directly.
| localStorage | sessionStorage | Cookies | |
|---|---|---|---|
| Lifetime | Until manually cleared | Until tab closes | Until expiry you set |
| Size limit | ~5-10MB | ~5-10MB | ~4KB |
| Sent to server automatically? | No | No | Yes, on every request |
| Typical use | Long-lived client preferences | Per-tab draft state | Auth sessions, tracking |
localStorage.setItem('theme','dark');
sessionStorage.setItem('draftId','42');
document.cookie = 'sessionId=abc; Secure; SameSite=Lax';
017What is Web Components / Custom Elements at a high level?Advanced+
Easy explanation
Web Components are a native browser standard for building your own reusable, encapsulated HTML tags (like <my-rating-stars>) without needing a framework. They combine Custom Elements (defining the new tag and its JS behavior), Shadow DOM (style and markup encapsulation so your component's CSS can't leak out or be overridden accidentally) and HTML templates.
They're framework-agnostic — a Web Component built once can be dropped into a React app, a Vue app, or plain HTML — which is why design systems sometimes ship a Web Component version alongside framework-specific wrappers. The trade-off is more verbose, lower-level APIs compared to something like a React component, so most teams still reach for a framework for full applications and use Web Components for shareable, isolated widgets.
class RatingStars extends HTMLElement {
connectedCallback(){ this.innerHTML = '★★★★☆'; }
}
customElements.define('rating-stars', RatingStars);
CSS3 & Responsive Design
The CSS Box Model
Every element is content, wrapped in padding, wrapped in a border, wrapped in margin. box-sizing: border-box makes your declared width/height include padding and border, which is why most teams set it globally.
018Explain the CSS box model in detail.Beginner+
Easy explanation
Every HTML element is rendered as a rectangular box made of four layers, from inside out: the content (text/image), padding (space inside the border), border (the visible edge) and margin (space outside the border, between this box and its neighbors).
By default (box-sizing: content-box), the width you set only covers the content — padding and border get added on top, so a 300px-wide box with 20px padding and a 1px border actually takes up 342px. Setting box-sizing: border-box makes your declared width include padding and border, so a 300px box stays 300px no matter how much padding you add — this is why almost every modern CSS reset starts with `* { box-sizing: border-box; }`.
* { box-sizing: border-box; }
.card { width: 300px; padding: 20px; border: 1px solid #ccc; }
/* Total rendered width stays exactly 300px */
019Flexbox vs Grid — how do you choose?Beginner+
Easy explanation
Flexbox lays items out along a single axis — a row or a column — and is built for distributing space between items and aligning them, like a navbar, a button group, or a list of cards that wrap. Grid lays items out in two dimensions at once — rows AND columns together — and is built for whole-page or whole-component layouts where you need precise control over both axes, like a dashboard or a magazine-style layout.
A simple rule interviewers like: if you're thinking 'items in a row' or 'items in a column', reach for Flexbox. If you're thinking 'a grid of rows and columns', reach for Grid. In practice most real interfaces use both together — Grid for the page skeleton, Flexbox inside individual components.
| Flexbox | Grid | |
|---|---|---|
| Dimensions | One axis (row or column) at a time | Two axes together (rows and columns) |
| Best for | Navbars, button groups, wrapping cards | Page layouts, dashboards, image galleries |
| Item sizing driven by | Content size, unless you set flex-grow/shrink | Explicit track sizes (fr, px, %) you define upfront |
.row { display:flex; gap:1rem; align-items:center; }
.grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1rem; }
020What is CSS specificity and how is it calculated?Intermediate+
Easy explanation
When two CSS rules target the same element with conflicting properties, specificity decides which one wins (assuming they're not marked !important and appear in the same cascade layer/origin). It's usually described as four numbers: inline styles beat everything, then ID selectors, then classes/attributes/pseudo-classes, then element/tag selectors — you compare them left to right like a version number, not by adding them up.
A common mistake is styling components with heavy nested selectors like `#app .sidebar .card p` and then being unable to override a single paragraph's color from a simpler class elsewhere, because that long chain outranks it. The practical fix teams use today: keep selectors flat and low-specificity (mostly single classes), and reserve deep nesting or IDs for rare, deliberate overrides.
| Selector type | Example | Specificity weight |
|---|---|---|
| Inline style | style="color:red" | Highest — always wins unless !important is used elsewhere |
| ID | #header | 1,0,0 |
| Class / attribute / pseudo-class | '.card', '[type=text]', ':hover' | 0,1,0 |
| Element / pseudo-element | p, ::before | 0,0,1 |
#app .card p { color: black; } /* wins: id + class + tag */
.card p { color: gray; } /* loses even though it's written later */
021What is a media query and how do you write mobile-first CSS?Beginner+
Easy explanation
A media query is a CSS rule that only applies when a condition is true, most commonly the viewport width, letting you change layout for phones vs tablets vs desktops. Mobile-first means you write your default (un-queried) styles for the smallest screen, then use `min-width` queries to add complexity as the screen grows — the opposite of writing for desktop first and shrinking down with `max-width`.
Mobile-first is generally preferred because it forces you to prioritize content for constrained screens first, and it usually produces less CSS overall since you're only adding rules, not fighting existing desktop rules. Real teams typically define a small set of shared breakpoints (e.g. 640px, 768px, 1024px, 1280px) rather than a unique breakpoint per component.
/* mobile-first: default = phone */
.card { padding: 12px; }
@media (min-width: 768px) {
.card { padding: 24px; }
}
@media (min-width: 1024px) {
.card { padding: 32px; }
}
022What are CSS custom properties (variables) and how are they different from Sass variables?Intermediate+
Easy explanation
CSS custom properties (--accent-color) are variables defined and read directly in CSS with var(), and unlike Sass variables, they are live in the browser: they can be changed at runtime with JavaScript or overridden per-component/per-theme, and they follow the normal CSS cascade and inheritance rules.
Sass variables ($accent-color) only exist at compile time — they get replaced with their literal value before the CSS ever reaches the browser, so they can't be changed based on user interaction (like a dark-mode toggle) without recompiling. This is exactly why CSS custom properties are the standard choice for theming (light/dark mode) today, often defined once on :root and overridden inside a `.dark` class.
| CSS custom property (--x) | Sass variable ($x) | |
|---|---|---|
| When resolved | At runtime, in the browser | At build/compile time |
| Can change via JS? | Yes | No — needs a rebuild |
| Follows CSS cascade/inheritance? | Yes | No — plain text substitution |
:root { --accent:#d97757; }
.dark { --accent:#ff9d76; }
button { background:var(--accent); }
023What creates a new CSS stacking context, and why do z-index values sometimes 'not work'?Advanced+
Easy explanation
z-index only has meaning between elements that share the same stacking context — a new stacking context is created by things like a positioned element (relative/absolute/fixed/sticky) with a z-index set, an element with opacity less than 1, a CSS transform, filter, or explicit isolation: isolate.
The classic bug: you set z-index: 9999 on an element but it still appears behind something with z-index: 1, because that element's ancestor already created its own stacking context with a lower z-index — the child can never escape and be compared globally against elements outside its parent's context. Fixing it means raising the z-index (or removing the stacking-context trigger) on the correct ancestor, not just cranking up the child's number.
.modal-backdrop { position:fixed; z-index:100; } /* new stacking context */
.modal-content { position:relative; z-index:1; } /* only compared within .modal-backdrop */
024Explain position: relative, absolute, fixed and sticky.Intermediate+
Easy explanation
static (the default) follows normal document flow and ignores top/left/right/bottom. relative also stays in normal flow, but top/left/etc. now nudge it visually from where it would have been — and importantly, it becomes the anchor point for any absolutely positioned children. absolute removes the element from normal flow entirely and positions it relative to its nearest positioned ancestor (or the page if none exists).
fixed removes it from flow and positions it relative to the browser viewport, so it stays in place even while scrolling — used for sticky headers or floating action buttons. sticky behaves like relative until the element crosses a scroll threshold (like top: 0), at which point it 'sticks' like fixed within its parent's bounds — commonly used for section headers in a long list.
| Value | Stays in normal flow? | Positioned relative to |
|---|---|---|
| static (default) | Yes | N/A |
| relative | Yes (visually offset) | Its own original position |
| absolute | No | Nearest positioned ancestor |
| fixed | No | The browser viewport |
| sticky | Yes, until a threshold | Its scroll container, then acts fixed |
.nav { position:sticky; top:0; background:white; }
025What causes layout shift (CLS) and how do you prevent it?Advanced+
Easy explanation
Cumulative Layout Shift happens when visible content unexpectedly jumps around after the page has already started rendering — most often because an image, ad slot, embedded video or web font loads later and pushes everything below it down, or a banner gets injected above existing content.
Fixes: always reserve space for images/videos ahead of time using width/height attributes or aspect-ratio in CSS so the browser knows the final size before the file even downloads; avoid inserting new content above existing content unless it's in response to a user action; and use `font-display: optional` or preload critical fonts so text doesn't visibly reflow when a custom font swaps in.
img, video { width:100%; height:auto; aspect-ratio:16/9; }
/* Browser now reserves the correct box before the file loads */
026What are container queries and how are they different from media queries?Advanced+
Easy explanation
A media query only knows about the overall browser viewport size — it has no idea how big the specific component using it actually is. Container queries let a component style itself based on the size of its own containing element instead, so the exact same card component can render as a compact single column inside a narrow sidebar and as a wide two-column layout inside a full-width main area, without any JavaScript.
This solves a long-standing problem with reusable component libraries: previously you had to guess the viewport width and hope the component happened to be placed somewhere that made that width relevant. You opt a container in with `container-type`, then write `@container` rules the same way you'd write `@media` rules.
.wrapper { container-type: inline-size; }
@container (min-width: 500px) {
.card { display:grid; grid-template-columns:1fr 2fr; }
}
027What's the difference between em, rem, %, px and vw/vh units?Intermediate+
Easy explanation
px is an absolute unit — a fixed number of pixels regardless of context, so it doesn't scale with anything. em is relative to the font-size of its own parent element, which means nested ems can compound unpredictably (a common bug: nested lists where each level's font keeps shrinking). rem is relative to the root (html) element's font-size only, so it's predictable no matter how deeply nested it is — most teams use rem for spacing/typography specifically to avoid the em compounding problem.
% is relative to the parent's corresponding property (a width: 50% is half its parent's width). vw and vh are relative to the viewport's width/height (1vw = 1% of viewport width) and are useful for full-screen hero sections or text that should scale with the window, but should be used carefully for font sizes since they can make text unreadably small or huge without a min/max clamp.
| Unit | Relative to | Common use |
|---|---|---|
| px | Nothing — fixed | Borders, precise 1px details |
| em | Parent element's font-size | Component-internal spacing that should scale with local text size |
| rem | Root (html) font-size | Predictable global spacing and typography |
| % | Parent's own property (e.g. width) | Fluid widths |
| vw / vh | Viewport width/height | Full-bleed sections, fluid type with clamp() |
html { font-size: 16px; }
h1 { font-size: clamp(1.5rem, 4vw, 3rem); } /* fluid, but bounded */
028How does CSS Grid's fr unit and grid-template-areas work?Advanced+
Easy explanation
The `fr` unit represents a fraction of the remaining free space in a grid container after fixed-size tracks are accounted for — `grid-template-columns: 1fr 2fr` splits the leftover space into 3 parts and gives the second column twice as much as the first, which is far more predictable than juggling percentages that must add up to 100.
grid-template-areas lets you literally draw your layout as ASCII art, naming each grid area and then assigning `grid-area` names to your elements — this makes complex layouts (like a classic header/sidebar/main/footer page) extremely readable at a glance, and trivially rearrangeable for a different breakpoint just by redefining the area string.
.page {
display:grid;
grid-template-columns: 200px 1fr;
grid-template-areas:
"sidebar header"
"sidebar main";
}
.sidebar{grid-area:sidebar} .header{grid-area:header} .main{grid-area:main}
029What is the difference between visibility: hidden, display: none, and opacity: 0?Intermediate+
Easy explanation
display: none removes the element from the layout completely — it takes up no space, is not rendered, and is not reachable by screen readers or Tab key focus. visibility: hidden hides the element visually but it still takes up its layout space (an invisible gap remains), and it is also skipped by screen readers, but unlike display:none it can be selectively re-shown on a descendant with visibility: visible.
opacity: 0 makes the element fully transparent but it still occupies its space AND remains in the accessibility tree AND remains clickable/focusable — a common bug is an 'invisible' opacity:0 element still intercepting clicks or getting focused by Tab, which display:none or visibility:hidden would prevent.
| Takes up layout space? | Focusable / clickable? | Read by screen readers? | |
|---|---|---|---|
| display: none | No | No | No |
| visibility: hidden | Yes | No | No |
| opacity: 0 | Yes | Yes (bug risk!) | Yes (bug risk!) |
.truly-hidden { display: none; }
.invisible-but-present { opacity: 0; pointer-events: none; } /* fix the click bug */
030What are pseudo-classes vs pseudo-elements?Intermediate+
Easy explanation
A pseudo-class (single colon, like :hover, :focus, :nth-child(2)) selects an element based on a state or its position, not something you can target with a plain selector — 'this link, but only while the mouse is over it'. A pseudo-element (double colon, like ::before, ::after, ::first-line) lets you style or even insert a virtual sub-part of an element that doesn't exist as its own DOM node.
::before and ::after are extremely common for decorative content (icons, tooltips, custom bullet styling) that shouldn't be real DOM elements since they're not meaningful content — but because they can insert content via the `content` property, they should never be used to inject real information, since that content is invisible to some assistive tech and to copy-paste/select-all.
a:hover { color: var(--accent); }
.quote::before { content: "\201C"; color: var(--muted); }
031How would you build a responsive image gallery without a fixed number of columns hardcoded per breakpoint?Advanced+
Easy explanation
CSS Grid's `repeat(auto-fill, minmax(...))` (or `auto-fit`) lets the browser figure out how many columns fit, based only on a minimum column width you specify, with no media queries at all. `auto-fill` keeps empty tracks if there's leftover space (useful when you want consistent card width even with few items); `auto-fit` collapses empty tracks and stretches existing items to fill the row instead.
This pattern removes an entire category of media-query bugs where a fixed 3-column layout looks cramped on a 900px tablet or leaves huge gaps on an ultra-wide monitor — the grid adapts continuously instead of jumping at fixed breakpoints.
.gallery {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
032What is the cascade and how do CSS layers (@layer) change it?Advanced+
Easy explanation
The 'cascade' is the algorithm the browser uses to resolve which of several matching rules wins: first by origin/importance (user !important > author !important > author normal > user-agent default), then by specificity, then by source order (later wins ties). This is why two equally-specific rules resolve by whichever was declared last in the stylesheet.
@layer lets you explicitly group stylesheets (e.g. reset, base, components, utilities, overrides) and control their priority independent of specificity or source order — a low-specificity rule in a later layer will still lose to a high-specificity rule in an earlier layer only if you order the layers that way. This solves the long-standing pain of utility-class libraries (like Tailwind) needing !important hacks to beat component styles, by giving utilities their own high-priority layer instead.
@layer reset, base, components, utilities;
@layer components { .btn { color: blue; } }
@layer utilities { .text-red { color: red; } } /* wins even with equal specificity */
033How do you center a div — list at least three real methods.Beginner+
Easy explanation
This is a classic interview warm-up, but a strong candidate shows they know multiple tools and picks the right one for the situation rather than reciting one memorized trick. Flexbox centering (`display:flex; align-items:center; justify-content:center`) is the most common modern default for centering one or a few items inside a container.
Grid centering (`display:grid; place-items:center`) is even shorter for the same one-child case. The classic absolute + transform trick (`position:absolute; top:50%; left:50%; transform:translate(-50%,-50%)`) is useful when the parent can't become a flex/grid container itself, and margin: auto still works perfectly for horizontally centering a block element with a fixed width.
.center-flex { display:flex; align-items:center; justify-content:center; }
.center-grid { display:grid; place-items:center; }
.center-abs { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); }
034What is the difference between inline, inline-block and block display values?Beginner+
Easy explanation
block elements start on a new line, take the full available width by default, and respect width/height/margin/padding on all sides. inline elements flow within a line of text like a word would, ignore explicit width/height, and top/bottom margin/padding don't push other content away (though they still render visually).
inline-block is the hybrid: it flows inline with surrounding text/elements (doesn't force a new line) but DOES respect width, height, and full margin/padding like a block element — historically used for laying out nav items or buttons side-by-side before Flexbox existed, though Flexbox is generally preferred today.
span { display:inline-block; width:100px; padding:8px; } /* now width actually applies */
035What's the difference between CSS Grid's implicit and explicit grid?Advanced+
Easy explanation
The explicit grid is what you defined yourself with grid-template-columns/rows — a fixed set of tracks you planned for. The implicit grid is what the browser auto-generates when you place more items than your explicit tracks can hold, using default sizing rules (or the ones you set with grid-auto-rows/grid-auto-columns/grid-auto-flow).
Interview trap: if you define 3 explicit columns but render 10 items without extra rows defined, the browser will keep creating implicit rows automatically sized to fit their content — which is usually what you want, but can look wrong if you expected uniform row heights and forgot to set grid-auto-rows explicitly.
.grid {
display:grid;
grid-template-columns: repeat(3, 1fr); /* explicit */
grid-auto-rows: 120px; /* controls the *implicit* rows the browser adds */
}
JavaScript
The JavaScript Event Loop
JavaScript runs on one call stack. Async work (timers, fetch, file reads) is handed off to browser/Node APIs; when it finishes, its callback waits in a queue. The event loop only pushes a queued callback onto the stack once the stack is completely empty.
036var vs let vs const — explain scope, hoisting and reassignment.Beginner+
Easy explanation
var is function-scoped (or global if declared outside any function), meaning it 'leaks' out of if-blocks and for-loops into the surrounding function — a very common source of bugs. let and const are block-scoped, meaning they only exist inside the nearest { } they were declared in, matching how most other languages behave and what developers intuitively expect.
const doesn't mean the value is frozen — it means the variable binding can't be reassigned to a different value. You can still mutate an object or array stored in a const, you just can't do `myConst = somethingElse`. let allows reassignment. In modern codebases, const is the default choice, let is used only when you know you'll reassign, and var is essentially considered legacy.
| var | let | const | |
|---|---|---|---|
| Scope | Function (or global) | Block { } | Block { } |
| Can reassign? | Yes | Yes | No |
| Hoisting behavior | Hoisted & initialized to undefined | Hoisted but in 'temporal dead zone' until declared | Same as let |
const user = { name:'A' };
user.name = 'B'; // fine — mutating, not reassigning
let count = 0;
count = 1; // fine — reassignment allowed
037What is hoisting and what is the 'temporal dead zone'?Beginner+
Easy explanation
JavaScript scans a scope before running it and 'hoists' declarations to the top of that scope conceptually. For var, both the declaration AND an initial value of undefined are hoisted, so reading it before its line runs gives undefined instead of an error. For function declarations, the entire function body is hoisted, so you can call a function before its written position in the file.
For let and const, the declaration is hoisted but NOT initialized — the variable exists in a 'temporal dead zone' from the top of the block until the actual declaration line runs, and touching it during that window throws a ReferenceError. This is actually a safety feature: it turns a silent undefined bug (with var) into a loud, immediate error (with let/const).
console.log(a); // undefined (var is hoisted + initialized)
var a = 10;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
038What is a closure and give a real use case?Intermediate+
Easy explanation
A closure is what happens when a function 'remembers' the variables from the scope it was created in, even after that outer function has already finished running and would normally have its variables cleaned up. The inner function keeps a live reference to those outer variables instead of a snapshot copy.
A very practical real-world use case: creating private state. counter() below runs once and returns a function; every time you call that returned function, it still has access to its own private `n`, which nothing outside can reach or accidentally overwrite — this is how you build things like a debounce function, a memoization cache, or module-style encapsulation before ES modules existed.
function counter(){
let n = 0;
return () => ++n; // remembers 'n' forever
}
const next = counter();
console.log(next()); // 1
console.log(next()); // 2 — n persisted between calls
039Explain the JavaScript event loop with a concrete ordering example.Advanced+
Easy explanation
JavaScript has one call stack — it can only run one thing at a time. When you call an async API (setTimeout, fetch, a Promise), the actual waiting happens outside JavaScript (in the browser or Node's C++ layer); once it's done, JavaScript queues up the callback rather than running it immediately, because the stack might still be busy running your synchronous code.
There are two queues, and this is the part interviewers really test: the microtask queue (Promise .then/.catch/.finally, queueMicrotask) is fully drained after every single synchronous block AND before the next macrotask (setTimeout, setInterval, I/O) runs. That's why in the example, even a setTimeout(fn, 0) runs after a Promise.resolve().then(), despite both being 'async' and the timeout being scheduled first.
console.log('A');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('B');
// Output: A, B, promise, setTimeout
040Promise vs async/await — what's actually happening under the hood?Intermediate+
Easy explanation
A Promise is an object representing a value that will exist in the future (pending → fulfilled or rejected), and you consume it with .then()/.catch() chains. async/await is syntax sugar built entirely on top of Promises — an async function always returns a Promise itself, and `await` pauses execution of that function (without blocking the rest of the program) until the awaited Promise settles.
The real benefit of async/await isn't 'it's faster' (it's not — same event loop underneath) — it's that it lets you write asynchronous logic that reads top-to-bottom like synchronous code, including using regular try/catch for error handling instead of chaining .catch() everywhere, which is much easier to reason about for multi-step flows.
// Promise chain
fetch('/api').then(r=>r.json()).then(data=>console.log(data)).catch(err=>console.error(err));
// same thing with async/await
async function load(){
try {
const res = await fetch('/api');
const data = await res.json();
console.log(data);
} catch (err) { console.error(err); }
}
041What is event delegation and why is it more efficient?Intermediate+
Easy explanation
Instead of attaching a click listener to every single button in a list (which is wasteful and breaks for buttons added later dynamically), event delegation attaches ONE listener to a shared ancestor and relies on event bubbling — a click on a child bubbles up through its parents, and the listener checks `event.target` to figure out which specific child was actually clicked.
This is both more memory-efficient (one listener instead of a thousand) and automatically works for elements added to the DOM later, since the listener lives on the parent, not on each individual (possibly not-yet-existing) child — extremely useful for things like a dynamically rendered todo list where items are added and removed constantly.
document.querySelector('.list').addEventListener('click', e => {
if (e.target.matches('button.delete')) {
console.log('delete item', e.target.dataset.id);
}
});
042== vs === — explain type coercion with real examples.Intermediate+
Easy explanation
=== (strict equality) compares both value and type with no conversion — if the types differ, it's simply false, full stop. == (loose equality) first tries to convert one or both operands to a matching type before comparing, following a set of coercion rules that are genuinely hard to memorize perfectly and have produced some famously weird results in JavaScript.
Because of unpredictable results like `[] == false` being true, or `'' == 0` being true, almost every style guide and linter (ESLint's eqeqeq rule) mandates always using === and !== , with the narrow exception of `== null` as a shorthand to check for both null and undefined at once.
| Comparison | == result | === result |
|---|---|---|
| 0 == false | true (coerced) | false (different types) |
| '' == 0 | true (coerced) | false |
| null == undefined | true | false |
| [] == false | true (coerced twice) | false |
console.log(0 == false); // true — coercion
console.log(0 === false); // false — no coercion, different types
043map, filter and reduce — explain each with an example, and when reduce can replace both.Intermediate+
Easy explanation
map() transforms every item in an array into a new item and returns a new array of the same length — use it whenever you want 'the same list, but each item changed'. filter() tests every item with a condition and returns a new (possibly shorter) array containing only the items that passed — use it whenever you want 'a subset of this list'.
reduce() is the most general of the three: it walks through the array accumulating a single result (which could be a number, an object, or even a new array), by calling your function with the running total and the current item. Because map and filter can each be expressed as a special case of reduce, some interviewers ask you to implement filter using only reduce — a good way to prove you actually understand the accumulator pattern rather than just memorizing the three method names.
const nums = [1,2,3,4];
const doubled = nums.map(n => n*2); // [2,4,6,8]
const evens = nums.filter(n => n % 2 === 0); // [2,4]
const total = nums.reduce((sum,n) => sum+n, 0); // 10
// filter reimplemented with reduce:
const evens2 = nums.reduce((acc,n)=> n%2===0 ? [...acc,n] : acc, []);
044What is the prototype chain and how does 'class' syntax relate to it?Advanced+
Easy explanation
Every JavaScript object has an internal link to another object called its prototype, and when you access a property that doesn't exist directly on the object, JavaScript automatically looks it up on the prototype, then the prototype's prototype, and so on, until it finds it or reaches null — this chain is how methods like .map() are 'shared' by every array without being copied onto each one individually.
The `class` keyword introduced in ES6 didn't change this underlying model at all — it's syntax sugar over the exact same prototype-based inheritance that existed before. `class Dog extends Animal` still just sets up Dog.prototype's internal prototype link to Animal.prototype behind the scenes; it just reads more like inheritance in Java or Python.
const animal = { speak(){ return 'a generic sound'; } };
const dog = Object.create(animal); // dog's prototype is 'animal'
console.log(dog.speak()); // found via the prototype chain
class Animal { speak(){ return 'sound'; } }
class Dog extends Animal {} // same mechanism under the hood
045Debounce vs throttle — explain the difference and when to use each.Advanced+
Easy explanation
Both control how often a function runs in response to a high-frequency event (typing, scrolling, resizing), but they solve different problems. Debounce waits for a pause in activity before running — every new event resets the timer — so it's ideal for things like a search-as-you-type box where you only want to actually call the API once the user stops typing.
Throttle guarantees the function runs at most once every fixed interval no matter how many events fire in between, which is ideal for continuous events like scroll-position tracking or a resize handler where you want regular updates, but not one for every single pixel moved.
| Debounce | Throttle | |
|---|---|---|
| Runs when | Only after activity stops for X ms | At most once every X ms, even during continuous activity |
| Best for | Search input, autosave, form validation on typing | Scroll handlers, resize handlers, mouse-move tracking |
function debounce(fn, ms){
let id;
return (...args) => { clearTimeout(id); id = setTimeout(()=>fn(...args), ms); };
}
function throttle(fn, ms){
let ready = true;
return (...args) => { if(!ready) return; fn(...args); ready=false; setTimeout(()=>ready=true, ms); };
}
046What is the difference between call, apply and bind?Intermediate+
Easy explanation
All three let you explicitly control what `this` refers to inside a function, which matters because a regular function's `this` depends on HOW it's called, not where it's defined. call(thisArg, arg1, arg2, ...) invokes the function immediately with individual arguments listed one by one. apply(thisArg, [argsArray]) does the same but takes arguments as a single array — useful when you already have your arguments in array form.
bind(thisArg) is different from the other two: it doesn't call the function immediately — it returns a brand new function permanently bound to that `this`, which you can call later (or pass as a callback) without losing context, extremely common for event handlers inside class components before hooks/arrow functions made this less necessary.
function greet(greeting){ return `${greeting}, ${this.name}`; }
const user = { name:'Sara' };
greet.call(user, 'Hi'); // 'Hi, Sara'
greet.apply(user, ['Hi']); // 'Hi, Sara'
const bound = greet.bind(user);
bound('Hi'); // 'Hi, Sara' — later, still bound
047Explain the spread operator vs the rest parameter — they use the same '...' syntax but mean different things.Intermediate+
Easy explanation
Spread (...) EXPANDS an iterable (array, string, or an object's own enumerable properties) into individual elements — used when copying/merging arrays or objects, or passing an array's elements as separate function arguments. Rest (...) does the opposite: it COLLECTS multiple individual values into a single array — used in function parameters to gather 'everything else' passed in, or in destructuring to gather remaining properties.
The rule of thumb: if '...' appears where a value is being consumed/read (like inside an array literal or a function call), it's spread. If it appears where a value is being defined/received (like a function parameter list or the left side of a destructure), it's rest.
// spread: expanding
const a = [1,2]; const b = [...a, 3]; // [1,2,3]
const obj = {...{x:1}, y:2}; // {x:1,y:2}
// rest: collecting
function sum(...nums){ return nums.reduce((a,b)=>a+b,0); }
const {first, ...rest} = {first:1, second:2, third:3}; // rest = {second:2, third:3}
048What is the difference between deep copy and shallow copy, and why does spreading a nested object not fully copy it?Advanced+
Easy explanation
A shallow copy duplicates only the top level of an object or array — nested objects/arrays inside it are still the SAME reference shared between the original and the copy, so mutating a nested value through the copy also changes the 'original'. The spread operator ({...obj}) and Object.assign() both only do a shallow copy.
A deep copy recursively copies every nested level so there are zero shared references anywhere. Common ways to get a real deep copy: structuredClone(obj) (the modern built-in, works with most data types including dates/maps), or JSON.parse(JSON.stringify(obj)) (older, widely used, but silently drops functions, undefined values and Dates become strings).
const original = { profile: { age: 30 } };
const shallow = { ...original };
shallow.profile.age = 99;
console.log(original.profile.age); // 99 — the nested object was shared!
const deep = structuredClone(original); // truly independent copy
049What is the difference between synchronous and asynchronous JavaScript, and why is JS often called 'single-threaded but non-blocking'?Intermediate+
Easy explanation
JavaScript itself runs on a single thread, meaning it can only execute one line of your code at a time — there's no true parallel execution of your JS logic like you'd get with multiple threads in Java. Synchronous code runs immediately, line by line, and blocks everything else until it finishes.
Asynchronous operations (network requests, timers, file reads) don't block that single thread because the actual waiting is delegated outside of JavaScript, to the browser or Node's underlying C++ APIs — JavaScript just registers a callback and moves on to the next line immediately. This is what makes JS 'non-blocking' despite being single-threaded: it never sits idle waiting for I/O, it just gets notified later when the result is ready.
console.log('1');
setTimeout(() => console.log('2 (async, runs later)'), 1000);
console.log('3');
// Output: 1, 3, 2 — line 3 never waits for the timer
050What is 'this' and how does an arrow function's 'this' differ from a regular function's?Advanced+
Easy explanation
In a regular function, `this` is determined dynamically by HOW the function is called (its 'call site') — as a method (obj.method()), `this` is the object before the dot; as a plain function call, `this` is undefined in strict mode (or the global object otherwise); with call/apply/bind, `this` is whatever you explicitly set.
Arrow functions don't have their own `this` at all — they inherit `this` lexically from the surrounding (enclosing) scope where they were WRITTEN, not where they're called, and this can never be changed with call/apply/bind. This is exactly why arrow functions are so useful for callbacks inside class methods or React components: they automatically keep `this` pointing to the outer object instead of becoming undefined when used as a detached callback.
class Timer {
seconds = 0;
start(){
setInterval(function(){ this.seconds++; }, 1000); // BUG: 'this' is not the Timer here
setInterval(() => { this.seconds++; }, 1000); // correct: arrow keeps outer 'this'
}
}
051What are generators and what problem do they solve?Advanced+
Easy explanation
A generator (function* ... ) is a special function that can pause its own execution at a `yield` point and resume later exactly where it left off, instead of running start-to-finish in one go like a normal function. Calling a generator function doesn't run its body immediately — it returns an iterator object, and each call to .next() runs the function up to the next yield and returns that value.
They're the foundation async/await was actually built on top of internally, and they're still directly useful for things like lazily generating an infinite sequence (without computing all of it upfront), building custom iterables, or step-by-step state machines where you need to pause and resume complex logic.
function* idGenerator(){
let id = 1;
while(true){ yield id++; }
}
const gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
052What is memoization and how would you implement a simple memoize function?Advanced+
Easy explanation
Memoization is caching the result of an expensive function call keyed by its input arguments, so that calling it again with the exact same arguments returns the cached result instantly instead of recomputing it. It's a classic trade-off of memory for speed, and only makes sense for pure functions — ones that always return the same output for the same input and have no side effects.
A common interview follow-up: how do you key the cache when arguments are objects, not simple primitives? A basic implementation uses JSON.stringify(args) as the cache key, though that breaks down for very large objects, functions, or circular references, which is where a Map keyed by object reference sometimes works better instead.
function memoize(fn){
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if(cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const slowSquare = n => { for(let i=0;i<1e8;i++){} return n*n; };
const fastSquare = memoize(slowSquare);
053What are Sets and Maps, and how are they different from plain objects and arrays?Intermediate+
Easy explanation
A Set stores only unique values (duplicates are automatically ignored) and is ideal for deduplicating a list or checking existence very quickly, without the awkward 'use an object as a fake set' pattern developers used before ES6. A Map stores key-value pairs like a plain object, but unlike objects, a Map's keys can be ANY type — not just strings/symbols — including objects, functions, or other Maps, and it preserves insertion order reliably.
Maps also have a real .size property (objects need Object.keys(obj).length as a workaround) and are generally faster for frequent additions/removals of key-value pairs. Sets/Maps are the right tool whenever your 'keys' aren't naturally strings, or when uniqueness/insertion-order guarantees actually matter to your logic.
const uniqueTags = new Set(['js','css','js']); // Set(2) {'js','css'}
const cache = new Map();
const userObj = { id: 1 };
cache.set(userObj, 'cached data'); // object as a key — impossible with plain {}
054What is currying and partial application?Advanced+
Easy explanation
Currying transforms a function that takes multiple arguments into a sequence of functions that each take one argument, returning the next function until all arguments are supplied — `add(1)(2)(3)` instead of `add(1,2,3)`. Partial application is closely related but more flexible: it pre-fills SOME of a function's arguments upfront and returns a new function waiting for the rest, not necessarily one argument at a time.
The practical value: currying/partial application let you create specialized, reusable versions of a general function — for example, turning a generic `multiply(a,b)` into a reusable `double = multiply(2)` — which is a common pattern in functional-programming-flavored codebases and libraries like Redux for building configured middleware or selectors.
const curriedAdd = a => b => c => a+b+c;
curriedAdd(1)(2)(3); // 6
const multiply = (a,b) => a*b;
const double = multiply.bind(null, 2); // partial application
double(5); // 10
TypeScript
Where TypeScript sits in your build
TypeScript never runs directly — it is checked and compiled down to plain JavaScript. The type system exists purely at build/edit time to catch mistakes; at runtime, every type annotation is already gone.
055Why use TypeScript instead of plain JavaScript?Beginner+
Easy explanation
TypeScript adds a static type system on top of JavaScript: you describe the shape of your data (a function's parameters, an object's properties) and the compiler checks that your code actually matches those shapes BEFORE it ever runs, catching an entire category of bugs (calling a function with the wrong argument type, accessing a property that doesn't exist) at write-time instead of discovering them in production.
The other major benefit is tooling: because your editor knows the exact shape of every variable, it can offer accurate autocomplete, safely rename a property across an entire codebase, and immediately flag when a refactor breaks a caller somewhere else — this is what makes large, multi-developer codebases much safer to change than plain JS, where a typo in a property name only surfaces as `undefined` at runtime.
function add(a: number, b: number): number { return a + b; }
add(2, '3'); // TypeScript error caught before running, not a silent NaN at runtime
056interface vs type alias — what's the real difference and when do you pick one over the other?Beginner+
Easy explanation
Both can describe the shape of an object, and for basic object shapes they're nearly interchangeable. The two practical differences: interfaces support 'declaration merging' — declaring the same interface name twice automatically merges their properties together, which is useful for extending third-party library types — while type aliases cannot be redeclared. Interfaces are also generally considered slightly more idiomatic for public object/class contracts because their error messages tend to be a bit cleaner.
type aliases are strictly more powerful for anything that ISN'T a plain object shape: unions (`'idle' | 'loading'`), tuples, mapped types, and conditional types can only be expressed with `type`, not `interface`. Common convention: use interface for object shapes you expect other code to implement or extend, use type for everything else (unions, primitives, utility compositions).
| interface | type | |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions ('a'|'b') | No | Yes |
| Declaration merging | Yes (same name merges) | No — error if redeclared |
| Typical use | Public contracts, class shapes | Unions, tuples, utility compositions |
interface User { id: string; name: string; }
type Status = 'idle' | 'loading' | 'done'; // only possible with 'type'
057What are generics and why are they better than using 'any'?Intermediate+
Easy explanation
Generics let you write a function, class or type that works with many different types WHILE STILL preserving the relationship between its inputs and outputs — `function first<T>(items: T[]): T` says 'whatever type you pass in an array of, you get exactly that same type back out', which `any` cannot express because `any` throws away all type information entirely.
Using `any` instead of a generic silently disables type checking for that value everywhere it flows afterward — you lose autocomplete and error checking on anything derived from it. A generic keeps everything checked and accurate: if you call `first<number>([1,2,3])`, TypeScript knows the result is a number, not 'could be literally anything'.
function first<T>(items: T[]): T | undefined { return items[0]; }
const n = first([1,2,3]); // TypeScript infers T = number
const s = first(['a','b']); // TypeScript infers T = string
058What is a union type and how do you safely use it (type narrowing)?Intermediate+
Easy explanation
A union type (`string | number`) says a value could be one of several listed types, which mirrors real-world situations like an ID that's sometimes a string and sometimes a number from a legacy system. But you can't blindly call a string-only method on a union value, because TypeScript can't guarantee which branch it actually is at that point.
'Narrowing' is the process of using a runtime check (typeof, in, instanceof, or checking a shared literal field) so TypeScript can prove which specific type you're dealing with inside that code branch, after which it lets you safely use type-specific methods. This is TypeScript's core safety mechanism for handling real-world 'this could be one of several things' data.
function printId(id: string | number){
if (typeof id === 'string') {
console.log(id.toUpperCase()); // safe: narrowed to string here
} else {
console.log(id.toFixed(2)); // safe: narrowed to number here
}
}
059any vs unknown — why is unknown considered the safer 'escape hatch'?Intermediate+
Easy explanation
`any` completely opts a value out of type checking — you can call any method, access any property, and pass it anywhere, and TypeScript will never complain, which defeats the entire purpose of using TypeScript for that value and anything downstream of it. `unknown` also accepts a value of literally any type coming IN, but it forces you to narrow it (with typeof, instanceof, or a type guard) before you're allowed to do anything with it.
This makes `unknown` the correct type for genuinely unpredictable data — like the response body of an external API, or user input from a form — because it forces you to explicitly validate the shape before trusting it, whereas `any` would let unvalidated, potentially malformed data flow silently through your whole application.
function parseConfig(value: unknown){
if (typeof value === 'string') {
return value.trim(); // only allowed after narrowing
}
throw new Error('Invalid config');
}
060What is 'never' and when would you actually use it?Advanced+
Easy explanation
`never` represents a value that can genuinely never occur — a function typed to return `never` either always throws, always loops forever, or is a case that logically can't be reached. It's different from `void` (a function that returns nothing meaningful, but does complete normally) — `never` means the function never even finishes normally at all.
Its most useful practical application is 'exhaustiveness checking': in a switch statement over a union type, assigning the value to a variable typed `never` in the default case will cause a compile error if you later add a new type to the union and forget to handle it in the switch — turning a silent runtime gap into an immediate compile-time reminder.
type Shape = { kind:'circle'; r:number } | { kind:'square'; s:number };
function area(shape: Shape): number {
switch(shape.kind){
case 'circle': return Math.PI * shape.r ** 2;
case 'square': return shape.s ** 2;
default:
const _exhaustive: never = shape; // compile error if a new Shape variant is added and unhandled
return _exhaustive;
}
}
061What are the most useful built-in utility types (Partial, Pick, Omit, Record)?Advanced+
Easy explanation
TypeScript ships several generic helper types that transform an existing type instead of forcing you to hand-write a near-duplicate. Partial<T> makes every property of T optional (great for an 'update' function where the caller only sends the fields that changed). Pick<T, Keys> builds a new type containing only the listed properties (great for a lightweight 'preview' shape). Omit<T, Keys> is the opposite — everything except the listed properties.
Record<Keys, ValueType> builds an object type where every key from Keys maps to ValueType — useful for things like a lookup table keyed by an enum or union of string literals. Reaching for these instead of redefining a near-identical interface keeps your types in sync automatically whenever the original type changes.
type User = { id:string; name:string; email:string; };
type UserPreview = Pick<User,'id'|'name'>; // { id, name }
type UserUpdate = Partial<User>; // all fields optional
type PublicUser = Omit<User,'email'>; // everything except email
type RolePermissions = Record<'admin'|'editor', string[]>;
062What is a discriminated union and why is it the standard way to model state in TypeScript?Advanced+
Easy explanation
A discriminated union is a union of object types that all share one common literal field (often called `kind`, `type` or `status`) with a different literal value in each variant. TypeScript can then use that single field to automatically narrow which exact shape you're dealing with in an if/switch, giving you full autocomplete and safety for the OTHER fields specific to that branch.
This is the standard, safe way to model something like an API request's state (idle/loading/success/error) — instead of one object with a bunch of optional fields (where nothing stops you from accidentally having both `error` and `data` set at once), each state variant only carries the fields that are actually valid for it, making impossible states genuinely impossible to construct.
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; message: string };
function render(state: RequestState){
if (state.status === 'success') return state.data; // data only exists here — safely narrowed
}
063What does the 'satisfies' operator do and why is it different from a type annotation?Advanced+
Easy explanation
A normal type annotation (`const config: Config = {...}`) forces the WIDENED declared type onto the variable, which means afterwards TypeScript only knows it as 'a Config', even if the literal you wrote was more specific — this can lose precise information like literal string values.
`satisfies` checks that your value is COMPATIBLE with a type, but keeps the more precise inferred type for the variable itself instead of widening it. This means you get both: a compile error if the object doesn't match the expected shape, AND full autocomplete/narrowing based on the actual literal values you wrote, which a plain annotation would have thrown away.
const config = { mode: 'dark' } satisfies { mode: 'dark' | 'light' };
// config.mode is inferred as the literal 'dark', not the wider 'dark'|'light'
064What are index signatures and mapped types?Advanced+
Easy explanation
An index signature (`[key: string]: number`) describes an object whose exact property names aren't known upfront, but whose VALUES all share a common type — useful for something like a dictionary/lookup object built dynamically at runtime. Without one, TypeScript will complain if you try to access a property by a dynamic string key that wasn't explicitly declared.
A mapped type builds a new type by iterating over the keys of an existing type and transforming each one — this is literally how utility types like Partial and Readonly are implemented internally. Understanding mapped types lets you write your own custom transformations, like a type that makes every property nullable, or turns every property into a getter function.
type Scores = { [studentName: string]: number };
const scores: Scores = { alice: 95, bob: 88 };
type Nullable<T> = { [K in keyof T]: T[K] | null }; // custom mapped type
type NullableUser = Nullable<{name:string; age:number}>; // { name: string|null; age: number|null }
065What is type inference and when should you still add explicit annotations?Intermediate+
Easy explanation
TypeScript can automatically figure out ('infer') a variable's type from the value assigned to it, without you writing an explicit annotation — `const age = 30` is automatically typed as `number`, no `: number` needed. This keeps code concise for local variables where the type is obvious from context.
You should still add explicit annotations for function parameters (TypeScript can't guess what callers will pass in), function return types on public/exported functions (locking down the contract so an accidental internal change doesn't silently widen the return type for every caller), and any variable initialized as an empty/ambiguous value like `let items = []` where inference alone would default to `any[]`.
const age = 30; // inferred: number — no annotation needed
function getUser(id: string): User { /* ... */ } // annotate parameters + public return types
066What's the difference between an enum and a union of string literals in TypeScript?Advanced+
Easy explanation
A TypeScript `enum` creates an actual JavaScript object at runtime that exists in your compiled output — you can loop over its values, and by default it also generates a reverse mapping from value back to name, which adds real runtime code even though it feels like 'just a type'. A union of string literals (`type Status = 'idle'|'loading'|'done'`) is purely a compile-time construct — it produces zero runtime JavaScript at all.
Many teams today prefer string literal unions over enums specifically because they compile to nothing extra, they work more naturally with plain JSON/APIs (a literal union value is just a normal string), and they avoid some well-documented quirks of TypeScript's numeric enums (like accidentally allowing any number to be assigned).
// enum — creates real runtime code
enum Status { Idle, Loading, Done }
// string literal union — zero runtime cost, preferred by many teams
type StatusLiteral = 'idle' | 'loading' | 'done';
067What is declaration merging and why do @types packages rely on it?Advanced+
Easy explanation
Declaration merging is TypeScript's ability to combine multiple declarations of the same name (interfaces, namespaces) into one single definition, rather than the second one overwriting or conflicting with the first. This mostly matters for interfaces: `interface Window { myGlobal: string }` declared in your own code merges with the built-in Window interface, adding your property to it without needing to modify the library's own type definitions.
This is exactly how the `@types/*` ecosystem lets you extend third-party or built-in types safely — for example, adding a custom property to Express's Request object (like `req.user` after an auth middleware runs) by merging your own declaration into the library's existing interface, instead of forking the library's source.
// extending Express's Request type via declaration merging
declare global {
namespace Express {
interface Request { user?: { id: string } }
}
}
// now req.user is typed everywhere without editing Express's own types
068How does TypeScript work with React — explain typing props, state and event handlers.Advanced+
Easy explanation
For component props, you typically define an interface or type describing exactly what a component accepts, then annotate the function's parameter with it — this gives you compile-time errors if a parent forgets a required prop or passes the wrong type, and autocomplete for every prop when using the component elsewhere. For useState, TypeScript infers the type from the initial value automatically, but you should explicitly annotate with a generic (`useState<User | null>(null)`) when the initial value doesn't reveal the full possible type range.
For event handlers, React ships specific event types (React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>) rather than using the generic DOM Event type, because these give you correctly-typed access to fields like `event.target.value` that only exist on specific element/event combinations.
interface UserCardProps { name: string; onSelect?: (id: string) => void; }
function UserCard({ name, onSelect }: UserCardProps){
const [user, setUser] = useState<User | null>(null); // explicit generic, since null doesn't reveal shape
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => console.log(e.target.value);
return <input onChange={handleChange} />;
}
React.js
React's Render & Reconciliation Cycle
React never re-renders the whole real page. It builds a lightweight virtual description of the UI, compares (diffs) it against the previous version, and applies only the minimal set of real DOM changes needed — which is far cheaper than touching the DOM directly for every change.
069What is a React component and what's the difference between function and class components?Beginner+
Easy explanation
A component is a reusable, self-contained piece of UI that takes inputs (props) and describes what should appear on screen, typically returning JSX. React composes an entire application out of components nested inside each other, from a single button up to the whole page.
Class components (extends React.Component) were the original way to hold state and lifecycle logic, using this.state and methods like componentDidMount. Function components with Hooks (useState, useEffect, etc.), introduced in React 16.8, can now do everything class components could, with less boilerplate and easier logic reuse — as a result, virtually all new React code today is written as function components, and class components are mostly only seen in older codebases.
// Function component (modern default)
function Welcome({ name }) {
return <h1>Hello, {name}</h1>;
}
// Class component (legacy style, still valid)
class WelcomeClass extends React.Component {
render(){ return <h1>Hello, {this.props.name}</h1>; }
}
070Props vs state — what's the fundamental difference?Beginner+
Easy explanation
Props (short for properties) are inputs passed INTO a component from its parent — a component never modifies its own props; they're read-only from that component's point of view, similar to function arguments. State is data a component owns and manages itself, and it CAN change over time, usually in response to user interaction or a network response, using useState or useReducer.
A simple test interviewers use: if a value needs to be shared with or configured by a parent, it's a prop. If a value only changes because of something happening inside this specific component (a toggle, a form field's current text, a counter), it's state. Mixing these up — like trying to reassign a prop directly — is a common beginner bug React will actively warn against.
function Counter({ startAt }){ // startAt is a prop — set by the parent, read-only here
const [count, setCount] = useState(startAt); // count is state — owned and changed here
return <button onClick={()=>setCount(c=>c+1)}>{count}</button>;
}
071What does useEffect do, and what is the dependency array actually for?Intermediate+
Easy explanation
useEffect lets a component 'synchronize' with something outside of React's own rendering — fetching data, subscribing to an event, manually manipulating a non-React DOM element, or setting document.title. It runs AFTER the browser has painted the render, not during rendering itself, which keeps rendering pure and fast.
The dependency array (the second argument) tells React when to re-run the effect: `[]` means 'run once after the first render, never again', `[count]` means 'run again any time count changes between renders', and omitting the array entirely means 'run after every single render' (rarely what you want). Getting the dependency array wrong — omitting a value your effect actually uses — is one of the most common React bugs, which is why the exhaustive-deps ESLint rule exists to catch it.
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => { /* optional cleanup, runs before the next effect or on unmount */ };
}, [count]); // re-runs only when 'count' changes
072Why do list items need a 'key' prop, and what happens if you use the array index as the key?Intermediate+
Easy explanation
When React re-renders a list, it needs to match up the NEW list of elements against the OLD list to figure out what actually changed, was added, or was removed — the `key` prop gives each item a stable identity across renders so React can do this matching correctly, instead of naively assuming position in the array means the same item.
Using the array index as a key works fine for a static list that never reorders, but breaks badly for a list that can be reordered, filtered, or have items inserted/removed from the middle — because the index of every item AFTER the change shifts, React thinks those items 'changed' when they didn't, causing components to lose their internal state (like an input's current text) or re-render unnecessarily. The fix: use a genuinely stable, unique ID from your data (like a database id), not the array position.
// risky if the list can reorder/insert/remove:
items.map((item, i) => <Row key={i} item={item} />);
// safe: a stable identifier tied to the actual data
items.map(item => <Row key={item.id} item={item} />);
073Controlled vs uncontrolled inputs — what's the difference and when would you use each?Intermediate+
Easy explanation
A controlled input's value is driven entirely by React state — you set `value={state}` and update state on every `onChange`, so React is the 'single source of truth' for what's in the field at all times, letting you easily validate, transform, or reset it programmatically. An uncontrolled input keeps its value in the actual DOM (like a normal HTML input) and you only reach in to READ its current value when needed, usually via a ref.
Controlled inputs are the default recommendation for most forms because they make validation, conditional formatting, and syncing multiple fields trivial. Uncontrolled inputs are simpler and occasionally more performant for very large forms with many fields where you only care about the values at submit time, not on every keystroke, since they avoid triggering a React re-render on every character typed.
| Controlled | Uncontrolled | |
|---|---|---|
| Source of truth for the value | React state | The DOM itself |
| Read value via | state variable directly | a ref, usually only at submit time |
| Best for | Live validation, syncing fields, conditional UI | Very large/simple forms, quick migrations from plain HTML |
// controlled
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />
// uncontrolled
const nameRef = useRef();
<input ref={nameRef} defaultValue="" />
// later: nameRef.current.value
074What is 'lifting state up' and why is it necessary?Intermediate+
Easy explanation
When two sibling components both need access to the same piece of data (or one needs to react to a change the other causes), React data flows one direction: down through props. Since siblings can't talk to each other directly, 'lifting state up' means moving that shared piece of state to their nearest common parent, which then passes the value down as a prop to both, and passes a callback function down so children can request a change.
This keeps React's unidirectional data flow intact and predictable — you never have two separate 'sources of truth' for the same logical piece of data drifting out of sync. When lifting state up starts to feel awkward because the shared state needs to travel through many layers of components that don't otherwise need it ('prop drilling'), that's usually the signal to reach for Context or a state-management library instead.
function Parent(){
const [query, setQuery] = useState('');
return (
<>
<SearchBox value={query} onChange={setQuery} />
<ResultsList query={query} /> {/* sibling reacts to the lifted state */}
</>
);
}
075What is Context, and what's the trade-off of using it too broadly?Intermediate+
Easy explanation
Context lets a value be read by any descendant component in the tree without manually passing it down as a prop through every intermediate layer ('prop drilling') — you wrap a subtree in a Provider with a value, and any descendant can read it with useContext, no matter how deeply nested.
The trade-off: by default, EVERY component consuming a Context re-renders whenever that Context's value changes, even if the specific piece of data that component actually uses didn't change — so putting a large, frequently-changing object in one big Context (instead of several smaller, more targeted ones, or a proper state library) can cause widespread unnecessary re-renders across the app. This is exactly why Context is best suited for genuinely 'global-ish' and relatively stable values (theme, current user, locale), not high-frequency state like live form input.
const ThemeContext = createContext('light');
function App(){
return <ThemeContext.Provider value="dark"><Toolbar/></ThemeContext.Provider>;
}
function Toolbar(){
const theme = useContext(ThemeContext); // no prop drilling needed
return <div className={theme}>...</div>;
}
076useMemo vs useCallback — what's the actual difference, and when do they NOT help?Advanced+
Easy explanation
Both are performance-optimization hooks that skip recalculating something on every render unless their dependencies actually changed. useMemo memoizes a COMPUTED VALUE — useful for an expensive calculation (like filtering/sorting a huge array) that shouldn't rerun every single render. useCallback memoizes a FUNCTION REFERENCE itself — useful when you're passing a callback down to a child that's wrapped in React.memo, so the child doesn't think 'a new prop arrived' and re-render just because a brand-new function object was created.
The important nuance interviewers look for: these hooks are NOT free — they have their own memory and comparison overhead, so wrapping every single value/function in useMemo/useCallback 'just in case' can actually make things slightly slower, not faster. They should be reached for specifically when you've identified a measurable, real cost — an expensive computation, or breaking memoization on a genuinely expensive child component — not applied by default everywhere.
const sortedItems = useMemo(() => expensiveSort(items), [items]); // only resorts when 'items' changes
const handleSave = useCallback(() => saveItem(id), [id]); // stable reference for a memoized child
077What is React's reconciliation algorithm and how does the 'key' prop fit into it?Advanced+
Easy explanation
Reconciliation is the process React uses to compare a newly rendered element tree against the previous one and figure out the minimal set of real DOM operations needed to update the page, instead of tearing down and rebuilding everything from scratch on every render. Two major heuristics make this fast: elements of a DIFFERENT type at the same position are assumed to be entirely different (React discards the old subtree and builds a fresh one, losing its internal state), while elements of the SAME type are assumed to be the same logical element and just get their changed props updated in place.
For lists specifically, `key` is what lets React apply that same-type/different-type logic PER ITEM instead of just by position — with correct keys, React can tell that item #3 moved to position #1 (and keep its component instance and internal state intact) instead of assuming positions 1-3 all 'changed' and needing new instances.
// switching element type at the same position resets any internal state:
return isEditing ? <Editor key="edit"/> : <Viewer key="view"/>;
// same type, different key = React treats it as a totally different instance:
<Profile key={userId} />
078What are Error Boundaries and what CAN'T they catch?Advanced+
Easy explanation
An Error Boundary is a component (implemented as a class with componentDidCatch/getDerivedStateFromError, since function components can't yet implement one directly, or via a framework/library wrapper) that catches JavaScript errors thrown DURING RENDERING anywhere in its child tree, logs them, and displays a fallback UI instead of the whole app crashing to a blank white screen.
Error Boundaries specifically do NOT catch errors inside event handlers (those need a normal try/catch), errors in asynchronous code like a setTimeout callback or a fetch's .then(), errors during server-side rendering, or errors thrown inside the Error Boundary component itself. This is a very common interview trap — a candidate who says 'Error Boundaries catch all errors' hasn't actually used one in production.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(){ return { hasError: true }; }
componentDidCatch(error, info){ console.error(error, info); }
render(){ return this.state.hasError ? <FallbackUI/> : this.props.children; }
}
079What is React.memo and when does it actually prevent a re-render?Advanced+
Easy explanation
React.memo wraps a component so React will skip re-rendering it if its props are shallowly equal to the props from the last render — meaning every top-level prop is === the previous one. This is useful for a component that's genuinely expensive to render and whose parent re-renders often for unrelated reasons.
A very common gotcha: if you pass an inline object, array, or function as a prop (`<Child data={{a:1}} />` or `<Child onClick={()=>...}/>`), a brand NEW object/function is created on every parent render, so the shallow equality check always fails and React.memo provides zero benefit — you'd need to also memoize that object/function with useMemo/useCallback in the parent for React.memo to actually take effect.
const ExpensiveList = React.memo(function ExpensiveList({ items }){
return items.map(i => <Row key={i.id} {...i} />);
});
// this defeats memo — a new array reference every render:
<ExpensiveList items={data.filter(x => x.active)} />
// fix: memoize the filtered array itself
const activeItems = useMemo(() => data.filter(x => x.active), [data]);
080What's the difference between useReducer and useState, and when would you reach for useReducer?Advanced+
Easy explanation
useState is best for simple, independent pieces of state — a single toggle, a single input value. useReducer centralizes state updates into a single reducer function `(state, action) => newState`, similar to Redux, which becomes valuable once you have several related pieces of state that update together, or update logic complex enough that scattering it across many separate setState calls gets hard to follow and easy to get out of sync.
useReducer also makes state transitions easier to test in isolation (a reducer is just a pure function you can call directly with sample actions) and easier to trace/debug, since every state change flows through one dispatch function with a named action type, instead of many scattered setter calls.
function reducer(state, action){
switch(action.type){
case 'increment': return { count: state.count + 1 };
case 'reset': return { count: 0 };
default: throw new Error('Unknown action');
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'increment' });
081What are custom hooks and what rule must every hook follow?Intermediate+
Easy explanation
A custom hook is simply a regular JavaScript function whose name starts with `use` and that calls other hooks inside it, letting you extract and reuse stateful logic (like 'track window width' or 'debounce a value') across multiple components without duplicating the code or wrapping components in awkward higher-order-component patterns.
All hooks — built-in or custom — must follow the 'Rules of Hooks': only call hooks at the top level of a component or another hook (never inside loops, conditions, or nested functions), and only call them from React function components or other custom hooks (never from a regular JS function). This ensures React can reliably match up hook calls to the same internal state slot on every single render, in the same order every time.
function useWindowWidth(){
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}
// reused in any component: const width = useWindowWidth();
082What is the virtual DOM and is it actually 'faster than the real DOM'?Intermediate+
Easy explanation
The virtual DOM is a lightweight, plain-JavaScript-object description of what the UI should look like — React builds a new one on every render and diffs it against the previous one to compute the minimal set of REAL DOM operations required, then applies only those. Direct real-DOM manipulation is comparatively expensive because it can trigger layout recalculation and repainting, so batching and minimizing real DOM writes is genuinely valuable.
The honest, nuanced answer interviewers want: the virtual DOM itself isn't inherently 'faster than the DOM' in every possible case — a hand-optimized, surgical direct DOM update can sometimes beat it. Its real value is that it gives React a reliable, general-purpose way to compute a near-optimal set of DOM updates automatically, for developers who are simply describing 'what the UI should look like' declaratively rather than manually tracking and applying every individual DOM mutation themselves.
// You just describe the desired UI —
// React's virtual DOM diffing figures out the minimal real DOM changes:
function Badge({ count }){ return <span>{count}</span>; }
083What is Suspense and how does it relate to lazy loading and data fetching?Advanced+
Easy explanation
Suspense lets a component tree 'wait' for something before showing it, displaying a fallback UI in the meantime, without you manually wiring up loading-state booleans everywhere. Its most established use is code-splitting with React.lazy() — the component's code is only downloaded when it's actually needed, and Suspense shows a spinner/fallback while that download happens.
Newer frameworks (and React's own data-fetching integrations) extend Suspense to also cover DATA loading, not just code loading — a component can 'suspend' while its data is being fetched, letting a parent Suspense boundary show one shared loading state for multiple children instead of each one managing its own isLoading flag independently.
const Editor = React.lazy(() => import('./Editor'));
function Page(){
return (
<Suspense fallback={<Spinner/>}>
<Editor/>
</Suspense>
);
}
084What is prop drilling and what are the main ways to avoid it?Intermediate+
Easy explanation
Prop drilling is when a piece of data has to be passed down through several layers of components purely to reach a deeply nested child that actually needs it, even though the intermediate components don't use that data themselves — it clutters those components' prop lists and makes refactoring the tree's structure risky, since moving a component can silently break the data path.
Common fixes, roughly in order of how 'global' the problem is: Context (for moderately shared, relatively stable data like theme/auth), a dedicated state-management library like Redux/Zustand/Jotai (for complex, frequently-updated, widely shared application state), or simply restructuring the component tree so the data-owning component and the data-needing component are closer together (composition, passing components as children/props instead of raw data).
// prop drilling: Page doesn't use 'user' itself, just passes it through
<Page user={user}><Sidebar user={user}><Avatar user={user}/></Sidebar></Page>
// fix with Context: Avatar reads directly, no drilling through Page/Sidebar
const UserContext = createContext(null);
085What's the difference between useLayoutEffect and useEffect?Advanced+
Easy explanation
Both let you run side effects after rendering, but the TIMING differs. useEffect runs asynchronously AFTER the browser has already painted the updated screen to the user — the user might briefly see the 'before' state flash for a fraction of a second before your effect updates something visually.
useLayoutEffect runs SYNCHRONOUSLY right after React has updated the DOM, but BEFORE the browser paints anything to the screen — meaning if you need to measure or immediately mutate the DOM based on that measurement (like reading an element's height and then repositioning a tooltip before the user ever sees the wrong position), useLayoutEffect avoids any visible flicker. The trade-off is that it blocks the browser from painting until it finishes, so it should only be used for genuinely visual, synchronous DOM read/write work — useEffect should remain the default for everything else.
useLayoutEffect(() => {
const height = tooltipRef.current.getBoundingClientRect().height;
setPosition(calculatePosition(height)); // applied before the browser paints — no flicker
}, []);
Node.js, Express & REST APIs
A REST API Request's Journey
A well-built API never lets raw client input reach the database directly. Authentication confirms who is asking, validation confirms the data is well-formed, and only then does business logic touch persistent storage.
086What is Node.js and why is it a good fit for I/O-heavy backends?Beginner+
Easy explanation
Node.js is a JavaScript runtime (built on Chrome's V8 engine) that lets you run JavaScript outside the browser — on a server, in a CLI tool, anywhere. It ships with built-in modules for networking, the filesystem, and process management, which is what makes it usable for backend services and tooling, not just browser scripts.
Its event-driven, non-blocking I/O model means a single Node process can handle many concurrent requests without dedicating a separate thread to each one — while one request is waiting on a database query or a file read, Node is free to keep processing other requests instead of sitting idle. This makes it especially strong for I/O-heavy workloads (APIs, real-time apps, streaming) where most of the time is spent waiting on external things rather than doing heavy CPU computation.
import http from 'node:http';
http.createServer((req, res) => {
res.end('Hello from Node');
}).listen(3000);
087What is the Node.js event loop and how is it different from the browser's?Intermediate+
Easy explanation
Node's event loop follows the same core idea as the browser's: JavaScript runs on a single thread, and asynchronous operations (file I/O, network calls, timers) are delegated elsewhere and their callbacks are queued to run once the current stack is clear. Node's underlying implementation (libuv) additionally manages a thread pool behind the scenes for certain operations (like file system access on some platforms) even though your JavaScript code itself still appears single-threaded.
Node also has some phases and queue types the browser doesn't expose the same way — like `process.nextTick()` (runs even before Promise microtasks) and `setImmediate()` (runs in the 'check' phase, generally after I/O callbacks in a given loop iteration). For most application code these details rarely matter, but they occasionally come up when precisely ordering startup logic or draining a queue before shutdown.
console.log('start');
process.nextTick(() => console.log('nextTick')); // even before promises
Promise.resolve().then(() => console.log('promise'));
setImmediate(() => console.log('immediate'));
console.log('end');
088CommonJS vs ES Modules in Node — what changes between require() and import?Intermediate+
Easy explanation
CommonJS (require/module.exports) was Node's original module system: it loads modules synchronously and resolves them at the moment `require()` is called during execution. ES Modules (import/export) are the official JavaScript language standard, loaded asynchronously and resolved through static analysis at parse time — before any code even runs — which is what enables tree-shaking (bundlers can determine exactly which exports are actually used and drop the rest).
A common practical gotcha: you can't use top-level `await` in CommonJS files, but you can in ES Modules. Also, ESM imports are 'live bindings' (if the exporting module later changes an exported variable, importers see the updated value), while CommonJS exports a snapshot object at the time of import. Node projects choose one by setting `"type": "module"` in package.json (or using .mjs/.cjs file extensions).
// CommonJS
const express = require('express');
module.exports = { add };
// ES Modules
import express from 'express';
export function add(a,b){ return a+b; }
089What are Node streams and why use them instead of loading a whole file into memory?Intermediate+
Easy explanation
A stream processes data incrementally, in small chunks, as it arrives — instead of waiting for an ENTIRE file, HTTP response body, or dataset to be fully loaded into memory before you can start working with it. This matters enormously for large files (a multi-gigabyte video, a huge CSV export) where reading the whole thing into memory at once could exhaust available RAM or make the user wait unnecessarily long before anything starts happening.
Readable streams (a file being read, an incoming request body) emit data events chunk by chunk; Writable streams (a file being written, an outgoing response) accept chunks to write. `.pipe()` connects a readable directly to a writable, automatically handling the flow of data (and, importantly, backpressure) between them without you manually managing buffers.
import { createReadStream, createWriteStream } from 'node:fs';
createReadStream('large-video.mp4').pipe(res); // streams straight to the HTTP response, chunk by chunk
090What is backpressure and why does it matter for streams?Advanced+
Easy explanation
Backpressure is the situation where a data producer (like a fast file read) generates data faster than a consumer (like a slow network connection, or a client on a poor connection) can actually process or send it. Without any coordination, the producer's data would pile up in memory faster than it can drain, eventually exhausting memory or crashing the process under heavy load.
Node's stream API has built-in backpressure handling: `.write()` on a writable stream returns `false` when its internal buffer is full, signalling the producer to pause; the writable then emits a `'drain'` event once it's ready for more. `.pipe()` handles all of this automatically for you, which is exactly why `.pipe()` (or the newer stream/promises pipeline API) is strongly preferred over manually wiring `data`/`write` events yourself.
readableStream.pipe(writableStream); // automatically pauses the source if the destination can't keep up
091What is Express.js middleware and how does the request/response cycle actually flow through it?Intermediate+
Easy explanation
Middleware in Express is simply a function with the signature `(req, res, next)` that runs during the lifecycle of a request, BEFORE the final route handler. Each middleware can inspect or modify the request/response objects, end the response early (like an auth check rejecting an unauthenticated request), or call `next()` to hand off control to the next function in the chain.
This chain-of-responsibility pattern is what lets you compose cross-cutting concerns — logging, authentication, body parsing, rate limiting — as small, independently reusable functions applied in order, instead of duplicating that logic inside every single route handler. Forgetting to call `next()` (and also not ending the response) is one of the most common Express bugs — the request just hangs forever with no response sent.
function logger(req, res, next){
console.log(`${req.method} ${req.url}`);
next(); // must call this or the request hangs
}
app.use(logger);
app.use(express.json()); // built-in middleware: parses JSON request bodies
app.get('/api/users', (req,res) => res.json([]));
092How does Express error-handling middleware work, and why does it need 4 parameters?Intermediate+
Easy explanation
Express recognizes error-handling middleware SPECIFICALLY by its function signature having 4 parameters: `(err, req, res, next)` — that's not just a convention, Express literally checks the function's arity (argument count) to decide whether to treat it as a regular or error-handling middleware. You trigger it by calling `next(err)` anywhere in your normal middleware/route chain instead of calling `next()` with no arguments, or by an unhandled thrown error in a synchronous route.
Centralizing error handling this way means every route doesn't need to duplicate its own try/catch-and-format-response logic — they can just pass errors along, and one place decides how to log them and what shape of error response to send back to the client, keeping error formatting consistent across the whole API.
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
} catch (err) { next(err); } // hands off to the error middleware below
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
093What is REST and what makes an API 'RESTful'?Intermediate+
Easy explanation
REST (Representational State Transfer) is an architectural style for designing network APIs around RESOURCES (nouns, like 'users' or 'orders') rather than actions (verbs), using the standard HTTP methods to express what you want to do to that resource, and standard status codes to express the result. A resource is typically addressed by a URL (/users/42), and the same URL supports different operations depending on the HTTP method used on it.
Truly 'RESTful' also implies statelessness — each request from a client contains all the information the server needs to process it, with no reliance on server-side session state stored in memory between requests (which is exactly why token-based auth like JWTs fits well with REST, and why horizontally scaling a REST API across many server instances is straightforward — any instance can handle any request).
GET /users -> list users
GET /users/42 -> get one user
POST /users -> create a user
PATCH /users/42 -> partially update a user
DELETE /users/42 -> delete a user
094GET vs POST vs PUT vs PATCH vs DELETE — explain each and their idempotency.Beginner+
Easy explanation
GET retrieves a resource and should never have side effects — it's both safe (no changes) and idempotent (calling it many times gives the same result). POST usually creates a new resource or triggers a non-idempotent action — calling it twice with the same body typically creates two separate resources, which is why POST is NOT considered idempotent by default.
PUT replaces an entire resource with the data you send — calling it repeatedly with the same body results in the same final state, so it IS idempotent. PATCH partially updates only the fields you send — whether it's idempotent depends on exactly what you're patching (setting a field to a fixed value is idempotent; incrementing a counter by a PATCH is not). DELETE removes a resource — also idempotent, since deleting an already-deleted resource still leaves it 'not existing', the same end state.
| Method | Purpose | Idempotent? |
|---|---|---|
| GET | Read a resource | Yes |
| POST | Create / trigger an action | No, usually |
| PUT | Replace a whole resource | Yes |
| PATCH | Partially update a resource | Depends on the operation |
| DELETE | Remove a resource | Yes |
fetch('/api/users', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(user) });
095What is idempotency and why does it matter for retrying failed network requests?Advanced+
Easy explanation
An operation is idempotent if performing it multiple times has exactly the same effect as performing it once — this matters enormously for handling network failures, because if a client sends a request and never receives a response (timeout, dropped connection), it genuinely doesn't know whether the server actually processed it or not, and the safe move is often just to retry.
Retrying a naturally idempotent operation (like PUT /users/42 with the same body, or DELETE) is safe by definition. Retrying a non-idempotent operation (like POST /payments) without protection could double-charge a customer — which is why APIs that accept payments or other sensitive creates commonly support an 'Idempotency-Key' header: the client generates a unique key per logical operation, and the server recognizes a repeated key and returns the original result instead of processing it again.
POST /payments
Idempotency-Key: 8d8f6b2e-... // same key on retry -> server returns the original result, doesn't double-charge
096What are the most important HTTP status codes to know, and how do they group?Intermediate+
Easy explanation
Status codes are grouped by their first digit: 2xx means success, 3xx means redirection, 4xx means the CLIENT made a mistake (bad request, missing auth, forbidden, not found), and 5xx means the SERVER failed to handle an otherwise valid request. Correctly choosing between a 4xx and 5xx is itself an important signal — returning 500 for a validation error (which is really a 400) hides the real cause and can trigger unnecessary alerting/retries on the client side.
A well-designed API is consistent and precise with these: 201 (not just 200) for a successful creation, 204 for a successful action with no response body, 401 specifically for 'you're not authenticated at all', and 403 specifically for 'you ARE authenticated but you're not allowed to do this' — mixing up 401 and 403 is a very common but meaningful mistake.
| Code | Meaning | When to use it |
|---|---|---|
| 200 OK | Success | A successful GET/PUT/PATCH with a response body |
| 201 Created | Success, new resource | A successful POST that created something |
| 204 No Content | Success, nothing to return | A successful DELETE, or an update with no body |
| 400 Bad Request | Client sent malformed/invalid data | Failed validation |
| 401 Unauthorized | Not authenticated at all | Missing or invalid credentials |
| 403 Forbidden | Authenticated but not allowed | Valid user, insufficient permissions |
| 404 Not Found | Resource doesn't exist | Wrong ID/URL |
| 409 Conflict | Request conflicts with current state | Duplicate email on signup |
| 429 Too Many Requests | Client is being rate-limited | Too many requests too fast |
| 500 Internal Server Error | Server-side failure | An unexpected exception |
res.status(201).json(createdUser);
res.status(404).json({ error: 'User not found' });
097How should server-side API input validation work, and why isn't TypeScript enough?Advanced+
Easy explanation
Server input validation checks that incoming data (request body, query params, headers) actually matches the shape, types, ranges and business rules your API expects, BEFORE that data is used anywhere — this protects against malformed requests, malicious payloads, and simple client bugs. It should happen at the very edge of your server, before the data reaches any business logic or database call.
TypeScript's types only exist at COMPILE TIME and are completely erased by the time your code actually runs — they give you zero protection against what an actual network request contains, since a request body arrives as raw, untyped JSON that TypeScript has no way to check at runtime. That's why real APIs pair TypeScript with a runtime validation library (Zod, Joi, Yup) that actually inspects the real data as it arrives and rejects anything that doesn't conform.
import { z } from 'zod';
const createUserSchema = z.object({ email: z.string().email(), age: z.number().min(13) });
app.post('/users', (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json({ error: result.error.flatten() });
// result.data is now validated AND correctly typed
});
098What is API pagination and how does cursor-based pagination differ from offset-based?Advanced+
Easy explanation
Pagination limits how much data a single API response returns, letting a client fetch data in manageable pages instead of one huge dump. Offset-based pagination (`?page=3&limit=20`, or `?offset=40&limit=20`) is simple to implement and reason about, but has a real correctness problem: if rows are being inserted or deleted while a client is paging through, items can be skipped or duplicated across pages because 'page 3' is only meaningful relative to a snapshot of the data that keeps shifting underneath.
Cursor-based (a.k.a. keyset) pagination (`?cursor=lastSeenId&limit=20`) instead asks for 'everything after this specific known item', which stays correct even while data changes, and is also typically much faster on large tables because the database can jump straight to that position using an index instead of counting/skipping through every prior row like OFFSET requires.
| Offset pagination | Cursor pagination | |
|---|---|---|
| Correctness under concurrent writes | Can skip/duplicate items | Stays consistent |
| Performance on large tables | Degrades — has to skip N rows | Stays fast — uses an index |
| Can jump to 'page 5' directly? | Yes | Not directly — sequential only |
GET /posts?cursor=lastSeenPostId&limit=20
099What is CORS and why does a browser block a request that Postman handles fine?Intermediate+
Easy explanation
CORS (Cross-Origin Resource Sharing) is a security mechanism enforced by BROWSERS, not servers — it restricts whether JavaScript running on one origin (domain+protocol+port) is allowed to read the response of a request made to a DIFFERENT origin, unless that server explicitly opts in with the right response headers. This is why Postman/curl (which aren't browsers and don't enforce this policy) can hit an API just fine, while a browser-based frontend on a different origin gets blocked.
The server must respond with headers like `Access-Control-Allow-Origin` naming which origins are allowed (or `*` for any), and for 'non-simple' requests (like ones using custom headers, or methods like PUT/DELETE), the browser first sends an automatic OPTIONS 'preflight' request to check permission before sending the real one. A common misconception: CORS is not an authentication mechanism — it doesn't stop a non-browser client (like a script or another server) from accessing your API at all, it only controls what a BROWSER'S JavaScript is allowed to read.
// server response header enabling cross-origin access from one specific frontend:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Credentials: true
100What is API versioning and what are the main strategies?Advanced+
Easy explanation
API versioning lets you make a genuinely BREAKING change to your API (removing a field, changing a response shape, changing behavior) without instantly breaking every existing client that hasn't updated yet — old clients keep talking to the old version while new clients (or clients that have updated) use the new one.
The three most common strategies: URL versioning (`/api/v2/users` — simple, very visible, but 'infects' every route with a version), header versioning (a custom header or the `Accept` media type specifies the version — keeps URLs clean but is less discoverable/debuggable), and no explicit versioning at all combined with strictly additive, backward-compatible changes only (adding new optional fields, never removing or renaming existing ones) — many teams find this last approach lets them avoid the operational overhead of maintaining multiple live versions entirely, reserving real versioning for genuinely unavoidable breaking changes.
GET /api/v2/users // URL versioning
GET /api/users
Accept: application/vnd.myapi.v2+json // header/media-type versioning
101What are Node.js worker threads and when do you actually need them?Advanced+
Easy explanation
Node's event loop is excellent for I/O-heavy work but has one real weakness: CPU-INTENSIVE synchronous JavaScript (heavy computation, image processing, complex parsing) blocks the single main thread completely, meaning EVERY other request being handled by that Node process has to wait until it finishes — this is a very different failure mode than I/O waiting, which never blocks the thread.
Worker threads let you run genuinely CPU-bound JavaScript on a SEPARATE thread, with its own V8 instance and event loop, communicating with the main thread via message passing (not shared memory by default, avoiding classic threading bugs). The rule of thumb: reach for worker threads specifically when you've identified real CPU-bound work blocking your event loop — for ordinary I/O-heavy request handling (the vast majority of typical API work), Node's normal async model already handles concurrency well without needing threads at all.
import { Worker } from 'node:worker_threads';
const worker = new Worker('./cpu-heavy-task.js');
worker.on('message', result => console.log('done:', result));
102What is graceful shutdown and why does it matter during deployments?Advanced+
Easy explanation
Graceful shutdown means that when a server process is told to stop (typically via a SIGTERM signal sent by your deployment platform or container orchestrator during a rolling deploy or scale-down), it doesn't just die instantly mid-request — it stops accepting NEW connections, gives IN-FLIGHT requests a chance to finish (usually with a timeout), closes database connections and other resources cleanly, and only then actually exits.
Without this, a deploy can abruptly cut off requests that were mid-flight, returning connection-reset errors to real users, or leave a database transaction in an inconsistent half-finished state. Most Node HTTP frameworks expose a `.close()` method specifically for this — you call it on receiving SIGTERM, let the server finish serving currently-open connections, then exit the process.
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
server.close(() => console.log('HTTP server closed'));
await db.end(); // close DB pool cleanly
process.exit(0);
});
Databases: SQL & MongoDB
Relational vs Document Data Modeling
SQL splits related data into separate tables joined by keys, enforcing structure and consistency. MongoDB can embed related data directly inside one document when it's always read together, trading some duplication for fewer round trips.
103What is a relational database, and what does ACID mean?Beginner+
Easy explanation
A relational database (PostgreSQL, MySQL) stores structured data in tables made of rows and columns, with relationships between tables expressed through keys, and enforces a fixed schema (column names/types) before data can be inserted. It's a strong fit whenever your data has clear, stable relationships and you need strong guarantees about correctness.
ACID describes those correctness guarantees: Atomicity (a transaction either fully happens or not at all — no half-finished updates), Consistency (the database always moves from one valid state to another, respecting constraints), Isolation (concurrent transactions don't interfere with or see each other's incomplete work), and Durability (once committed, data survives even a crash immediately after). These guarantees are exactly why relational databases remain the default choice for things like financial transactions.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
104What is a primary key vs a foreign key?Beginner+
Easy explanation
A primary key uniquely identifies every row in a table — it can never be null, and no two rows can share the same value — and is what other tables use to reliably reference this specific row. A foreign key is a column in one table that references the primary key of another table, enforcing REFERENTIAL INTEGRITY: the database will reject an insert/update that would point to a primary key value that doesn't actually exist.
This relationship is the entire basis of relational modeling: instead of duplicating a user's full details inside every single order row, an `orders` table just stores the user's id as a foreign key, and you JOIN the tables together whenever you need the full picture — keeping each fact stored in exactly one place.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id), -- foreign key
total NUMERIC(10,2)
);
105INNER JOIN vs LEFT JOIN — explain with a concrete example.Intermediate+
Easy explanation
An INNER JOIN returns only rows where a match exists in BOTH tables — if a user has no orders, that user simply won't appear at all in the joined result. A LEFT JOIN keeps EVERY row from the left (first-named) table regardless of whether a match exists on the right, filling in NULL for any columns that come from the unmatched right-hand table.
A very common real interview question: 'Write a query to find every user, including ones who've never placed an order.' This is a textbook LEFT JOIN case — an INNER JOIN would silently drop exactly the users you're trying to find, since they have zero matching rows in the orders table.
| INNER JOIN | LEFT JOIN | |
|---|---|---|
| Returns rows where... | A match exists in both tables | Every row from the left table, matched or not |
| Unmatched right-side columns | N/A — row is excluded entirely | Filled with NULL |
-- includes users with zero orders (total shows as NULL)
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
106What is a database transaction and why do you need BEGIN/COMMIT?Intermediate+
Easy explanation
A transaction groups multiple SQL statements into one atomic unit — either every statement in it succeeds and is permanently saved (COMMIT), or if anything fails partway through, everything done so far is undone (ROLLBACK), leaving the database exactly as it was before the transaction started. This matters whenever a single logical operation actually requires multiple separate writes to stay consistent.
The textbook example is transferring money between two accounts: debiting account A and crediting account B are two separate UPDATE statements, but they must both succeed or both fail together — if the process crashed after debiting A but before crediting B, money would simply vanish without a transaction wrapping both statements.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- both succeed together, or ROLLBACK undoes both if either fails
107What is an SQL index and what's the trade-off of adding more of them?Advanced+
Easy explanation
An index is a separate, ordered data structure (usually a B-tree) that the database maintains alongside a table, letting it find matching rows for a WHERE clause or ORDER BY without scanning every single row (a 'full table scan') — similar to how a book's index lets you jump straight to a page instead of reading the whole book to find a topic.
The trade-off: every index has to be updated on every INSERT/UPDATE/DELETE to that table, and takes up additional disk space — so indexing a column that's rarely queried, or over-indexing a write-heavy table, actually slows down writes for little to no read benefit. The right approach is to index columns that are frequently filtered on or joined on in your ACTUAL query patterns, not every column defensively.
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
-- speeds up: SELECT * FROM orders WHERE user_id=42 ORDER BY created_at DESC;
108What is normalization, and when would you deliberately denormalize?Advanced+
Easy explanation
Normalization is the process of structuring relational data to minimize duplication — each fact is stored in exactly one place, and related data is split into separate tables connected by foreign keys, which prevents 'update anomalies' (like a user's name being stored in 500 order rows and only 499 of them getting updated when they change their name).
Denormalization deliberately reintroduces some duplication in exchange for read performance — for example, storing a `total_price` column directly on an order rather than recalculating it from line items every single time it's read, or caching a `comment_count` on a post instead of running a COUNT(*) on every page load. This is a conscious trade-off made for known, frequent read patterns, not a mistake — the risk you accept is that the duplicated data can drift out of sync if you're not careful to update it everywhere it's stored.
-- normalized: total is always calculated from line items (always correct, slower to read)
SELECT SUM(price*qty) FROM order_items WHERE order_id=42;
-- denormalized: total is pre-stored on the order (fast to read, must be kept in sync on write)
SELECT total FROM orders WHERE id=42;
109What is a window function and how is it different from GROUP BY?Advanced+
Easy explanation
GROUP BY collapses multiple rows into ONE summary row per group — you lose access to the individual row details once you've aggregated. A window function computes an aggregate or ranking VALUE across a set of related rows ('a window') WITHOUT collapsing them — every original row stays in the result, just with an extra calculated column attached.
This is exactly what you need for something like 'show me every order, along with that customer's rank among all their own orders by amount' — you need both the individual order details AND a calculated rank relative to a group, which a plain GROUP BY simply cannot express because it would throw away the individual rows.
SELECT user_id, total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) AS rank_within_user
FROM orders;
-- every order row survives, each gets its rank among that user's own orders
110WHERE vs HAVING — what's the actual execution-order difference?Intermediate+
Easy explanation
WHERE filters individual rows BEFORE any grouping/aggregation happens — it can't reference an aggregate function result (like COUNT(*)) because those values don't exist yet at that stage of query execution. HAVING filters GROUPS, AFTER the GROUP BY and aggregation have already been computed — which is exactly why HAVING is the only place you can filter on an aggregate value like 'only show users with more than 5 orders'.
A common mistake: trying to write `WHERE COUNT(*) > 5` — this fails because COUNT(*) doesn't exist as a value until after grouping happens, which is later in the logical execution order than WHERE.
SELECT user_id, COUNT(*) AS order_count
FROM orders
WHERE status = 'paid' -- filters individual rows first
GROUP BY user_id
HAVING COUNT(*) > 5; -- then filters the resulting groups
111What is MongoDB and how is its data model fundamentally different from a relational database?Beginner+
Easy explanation
MongoDB is a document database: instead of rows in a rigid table, it stores flexible, JSON-like documents (technically BSON) inside 'collections'. Different documents in the SAME collection can have different fields entirely — there's no enforced schema at the database level by default, giving you more flexibility to evolve your data shape over time without a formal migration for every change.
This flexibility is a genuine trade-off, not a free upgrade: without careful discipline (or schema validation rules, or an ODM like Mongoose enforcing structure at the application layer), a collection can accumulate inconsistent document shapes over time that make querying and reasoning about the data harder — relational databases trade that flexibility for guaranteed structural consistency instead.
{ "_id": ObjectId(...), "name": "Sara", "skills": ["React","Node"] }
112Embedding vs referencing in MongoDB — how do you decide which to use?Intermediate+
Easy explanation
Embedding stores related data directly INSIDE the parent document (like storing a blog post's comments as an array field within the post document itself) — this lets you fetch everything you need in a single read with no extra queries, which is ideal when the embedded data is always accessed together with its parent and doesn't grow unboundedly large.
Referencing stores just an ID pointing to a document in another collection (similar to a SQL foreign key) — this is better when the related data is large, changes independently, is shared across many parents, or would make a single document grow too big (MongoDB has a 16MB per-document limit). The real skill being tested here isn't memorizing the rule, but modeling around your ACTUAL access patterns: 'what data do I read together, and how does it grow?'
| Embedding | Referencing | |
|---|---|---|
| Reads | One query gets everything | May need a second query or $lookup |
| Best for | Data always read together, bounded size | Large, independently-changing, or shared data |
| Risk | Document can grow too large / unbounded arrays | Extra query overhead, more like SQL joins |
// embedded (comments live inside the post):
{ _id:1, title:'...', comments:[{text:'Nice!'}] }
// referenced (comments are their own collection):
{ _id:1, title:'...' } // post
{ _id:101, postId:1, text:'Nice!' } // comment
113What is the MongoDB aggregation pipeline?Advanced+
Easy explanation
The aggregation pipeline processes documents through an ORDERED sequence of stages, where each stage transforms the data and passes it to the next — similar conceptually to piping commands together in a Unix shell. Common stages include $match (filter, like a WHERE), $group (aggregate, like GROUP BY), $sort, $project (reshape/select fields), and $lookup (join data from another collection, similar to a SQL JOIN).
Because it's a pipeline, stage ORDER matters a lot for both correctness and performance — putting a $match stage as early as possible reduces the number of documents every later stage has to process, the same way filtering before joining is more efficient in SQL.
db.orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$userId', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } }
]);
114What are MongoDB transactions and when do you actually need them?Advanced+
Easy explanation
MongoDB supports multi-document ACID transactions (since version 4.0), letting you group several writes ACROSS documents or even across collections into one atomic unit that either fully commits or fully rolls back — the same guarantee SQL transactions provide. Before this existed, a single document's own updates were always atomic on their own, but coordinating a change spanning MULTIPLE documents required careful application-level workarounds.
You need a real transaction specifically when a single logical operation must update multiple documents consistently — like moving inventory from one warehouse document to another. For most everyday operations touching just one document, MongoDB's single-document atomicity is already enough, and reaching for full transactions unnecessarily adds real performance overhead.
const session = await mongoose.startSession();
await session.withTransaction(async () => {
await Account.updateOne({_id:a}, {$inc:{balance:-100}}, {session});
await Account.updateOne({_id:b}, {$inc:{balance: 100}}, {session});
});
Authentication & Web Security
A Secure Login Flow
Authentication answers 'who are you'. Authorization answers 'what are you allowed to do'. A secure app checks both, on the server, on every single protected request — never trusting the client's UI state alone.
115Authentication vs authorization — what's the difference, and what status codes map to each failure?Intermediate+
Easy explanation
Authentication verifies WHO someone is — logging in with a password, a valid token, or a session cookie proves identity. Authorization decides WHAT that already-authenticated identity is allowed to do — whether this specific logged-in user can view, edit or delete this specific resource.
These map to two different HTTP status codes that are commonly confused: 401 Unauthorized means 'I don't know who you are at all' (missing or invalid credentials) — despite its name, it's really about authentication. 403 Forbidden means 'I know exactly who you are, and you're simply not allowed to do this' — that's the real authorization failure. Getting this distinction right in an interview signals real hands-on experience, not memorized theory.
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
if (!req.user.roles.includes('admin')) return res.status(403).json({ error: 'Not authorized' });
116What is a JWT and what does 'signed, not encrypted' actually mean?Intermediate+
Easy explanation
A JWT (JSON Web Token) is a compact, URL-safe token made of three parts (header.payload.signature) that encodes a set of claims (like a user id and an expiry time). The signature proves the token's INTEGRITY — that nobody has tampered with the payload since it was issued — because only the server (with its secret key) could have produced a signature that verifies correctly.
But the payload itself is only base64-ENCODED, not encrypted — anyone can decode and read a JWT's contents (paste one into jwt.io and see for yourself), they just can't forge a new one that passes verification without the server's secret. This is why you should never put sensitive data (passwords, secrets) directly inside a normal JWT payload, and why interviewers often ask candidates to correct the misconception that JWTs are 'encrypted'.
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.signature...
// decode the middle part (payload) with any base64 tool — it's plainly readable, just tamper-evident
117JWT-based auth vs traditional server-side sessions — what are the real trade-offs?Advanced+
Easy explanation
A server-side session stores the actual session data (user id, permissions) in a database or memory store keyed by an opaque session ID, which is what's stored in the client's cookie — the server looks up the session on every request. This makes revocation trivial (just delete the session server-side) and keeps sensitive data entirely off the client, but requires a shared session store when you scale to multiple server instances.
A JWT embeds the claims directly IN the token itself, so a server can verify it (checking the signature) without a database lookup at all — great for stateless, horizontally-scaled APIs. The real cost: revoking a single JWT before its natural expiry is genuinely hard, since the server never 'remembers' issuing it — you either need short expiry times plus refresh tokens, or you end up maintaining a revocation/blocklist anyway, which partially defeats the statelessness benefit.
| Server-side session | JWT | |
|---|---|---|
| Where data lives | Server (DB/memory), client holds only an opaque ID | Inside the token itself, client-side |
| Revocation | Instant — delete server-side | Hard — needs short expiry + refresh or a blocklist |
| Scaling across servers | Needs a shared session store | Naturally stateless |
Set-Cookie: session=opaque_random_id; HttpOnly; Secure; SameSite=Lax
118What do HttpOnly, Secure, and SameSite cookie flags actually protect against?Advanced+
Easy explanation
HttpOnly prevents JavaScript (document.cookie) from reading the cookie at all — this is a critical defense specifically against XSS, because even if an attacker manages to inject a malicious script into your page, that script still can't steal an HttpOnly session cookie. Secure ensures the cookie is only ever sent over an HTTPS connection, never plain unencrypted HTTP, protecting it from being intercepted on the network.
SameSite controls whether the cookie is sent along with cross-site requests at all — `Strict` never sends it cross-site, `Lax` sends it for top-level navigation (like clicking a link) but not for background cross-site requests, and `None` (requiring Secure) sends it everywhere. SameSite=Lax or Strict is a major defense specifically against CSRF, because it prevents a malicious site from silently riding on your authenticated session.
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
119What is XSS (Cross-Site Scripting) and how do you actually prevent it?Advanced+
Easy explanation
XSS happens when attacker-supplied content gets rendered as if it were trusted HTML/JavaScript in another user's browser — for example, a comment field that isn't escaped, letting an attacker submit `<script>stealCookies()</script>` as a 'comment' that then silently runs in every other visitor's browser session. Stored XSS (saved in a database and served to many users) is generally more dangerous than reflected XSS (only triggered via a crafted URL a victim has to click).
Prevention: always use safe rendering methods that automatically escape content (`element.textContent` instead of `element.innerHTML`, or a framework like React that escapes by default when rendering `{variable}`), never build HTML by string-concatenating untrusted input, and add a Content-Security-Policy header as a strong defense-in-depth layer that restricts which scripts are even allowed to execute, limiting the damage even if an escaping mistake slips through.
// dangerous: renders raw HTML, including any injected <script> tags
element.innerHTML = userComment;
// safe: always renders as plain text, never executes
element.textContent = userComment;
120What is CSRF (Cross-Site Request Forgery) and how is it different from XSS?Advanced+
Easy explanation
CSRF tricks a victim's browser into sending an unwanted, STATE-CHANGING request to a site where they're already authenticated — for example, a malicious page silently submits a hidden form to `yourbank.com/transfer` while the victim is logged into their bank in another tab, and the browser automatically attaches their real session cookie, making it look like a legitimate request from them. Unlike XSS, CSRF doesn't need to inject any code into the target site at all — it exploits the fact that cookies are sent automatically.
Defenses: SameSite=Lax/Strict cookies (covered above) block most CSRF by default in modern browsers; a CSRF token (a random value embedded in the page and required to be resubmitted with the request) proves the request actually originated from your own site's form, since an attacker's page has no way to read that token; and checking the Origin/Referer header on state-changing requests adds another layer.
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="server-generated-random-value">
...
</form>
<!-- server rejects the submission if the token doesn't match what it issued -->
121What is SQL injection and why do parameterized queries fully solve it?Advanced+
Easy explanation
SQL injection happens when untrusted user input is directly concatenated into a SQL query string, letting an attacker change the actual STRUCTURE of the query — the classic example is a login form where entering `' OR '1'='1` as the password turns a WHERE clause that should check credentials into one that's always true, letting the attacker log in as anyone.
Parameterized queries (a.k.a. prepared statements) solve this completely by sending the query structure and the user's data to the database SEPARATELY — the database engine treats the user's input purely as a literal VALUE to compare against, never as part of the SQL syntax itself, no matter what characters it contains. String-concatenating user input into SQL should be considered an automatic security bug, full stop, in any modern codebase.
// vulnerable: attacker input can change query structure
const q = `SELECT * FROM users WHERE email = '${email}'`;
// safe: email is always treated as a plain value, never as SQL syntax
db.query('SELECT * FROM users WHERE email = $1', [email]);
122Why must passwords be hashed, and why isn't a normal hash function like SHA-256 enough on its own?Advanced+
Easy explanation
Passwords must never be stored in plaintext, because a single database leak would instantly expose every user's real password (and since people reuse passwords across sites, that damage spreads far beyond just your app). Hashing turns a password into a one-way, irreversible value — the same password always produces the same hash, but you can't feasibly reverse a hash back into the original password.
General-purpose hash functions like SHA-256 are actually a bad fit for passwords specifically because they're designed to be FAST — which is exactly the wrong property here, since it makes brute-forcing every possible password by an attacker who's stolen your hash database much faster too. Password-specific algorithms like Argon2id or bcrypt are deliberately SLOW and also incorporate a random 'salt' per password (so two identical passwords don't produce identical hashes), which together make large-scale offline cracking attempts impractically slow.
import argon2 from 'argon2';
const hash = await argon2.hash(plainPassword); // slow by design, includes a random salt automatically
const valid = await argon2.verify(hash, submittedPassword);
123What is rate limiting and what are the common strategies for implementing it?Advanced+
Easy explanation
Rate limiting caps how many requests a given client (identified by IP, user account, or API key) can make in a given time window, protecting your API from brute-force login attempts, scraping, abuse, or simple accidental infinite-retry-loop bugs from overwhelming your servers or database.
Common algorithms: fixed window (count requests per fixed time block, like 'max 100 per minute' — simple, but allows a burst right at the boundary between two windows), sliding window (smooths that boundary issue out), and token bucket (each client has a bucket that refills at a steady rate and is spent per request, naturally allowing occasional bursts while still capping sustained rate). Returning a 429 status with a `Retry-After` header when a client is rate-limited is the standard, well-behaved way to communicate it back to a legitimate client.
// simplified fixed-window rate limit using Redis
const key = `rate:${userId}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60); // window resets after 60s
if (count > 100) return res.status(429).json({ error: 'Too many requests' });
124What is OAuth 2.0 and how is it different from just logging in with a password?Advanced+
Easy explanation
OAuth 2.0 is an AUTHORIZATION framework — it lets a user grant one application limited, specific access to their data held by ANOTHER service, without ever sharing their actual password with the first application. 'Sign in with Google' is the everyday example: your app never sees the user's Google password; Google itself authenticates the user and hands your app a scoped, revocable access token instead.
An important nuance: OAuth 2.0 on its own is about AUTHORIZATION (access to resources), not identity verification — that's why OpenID Connect (OIDC) was built as a thin identity layer ON TOP of OAuth 2.0 specifically to standardize 'who is this user', adding an ID token alongside the access token. Modern 'social login' buttons are almost always really using OIDC, not raw OAuth 2.0 alone, even though people colloquially just say 'OAuth login'.
// simplified OAuth 2.0 Authorization Code flow:
// 1. redirect user to provider's login/consent screen
// 2. provider redirects back with a one-time authorization code
// 3. your server exchanges that code (+ client secret) for an access token
125What is Content Security Policy (CSP) and how does it add a layer of defense against XSS?Advanced+
Easy explanation
CSP is an HTTP response header that tells the BROWSER exactly which sources of scripts, styles, images, and other resources are allowed to load or execute on your page — anything not explicitly allowed is blocked by the browser itself, regardless of how it ended up in the page's HTML.
This matters as DEFENSE IN DEPTH: even if an attacker somehow manages to sneak a malicious `<script src="evil.com/steal.js">` into your page (through an escaping bug you missed, or a compromised third-party widget), a strict CSP that only allows scripts from your own domain will simply refuse to load or run that injected script, limiting the real-world damage of an XSS bug that still made it through your other defenses.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
126What is the difference between hashing and encryption, and why can't you 'decrypt' a password hash?Intermediate+
Easy explanation
Encryption is a TWO-WAY operation: data is transformed with a key, and anyone holding the correct key can reverse it back to the original — used when you genuinely need to recover the original data later (like an encrypted database column you'll need to read back). Hashing is deliberately ONE-WAY: it's mathematically designed so you cannot reconstruct the original input from its hash, no matter what key or algorithm you have.
This is exactly why a 'forgot password' feature never sends you your OLD password back — the server literally cannot recover it from the stored hash, only verify a NEWLY submitted password by hashing it and comparing. Any service that emails you your actual original password is a strong signal they're storing passwords insecurely (in plaintext or with reversible encryption instead of a proper one-way hash).
// verifying a login is always: hash the NEW input and compare, never decrypt the stored value
const isValid = await argon2.verify(storedHash, submittedPassword);
Next.js
Where Server and Client Components Run
By default every component in the App Router runs on the server and never ships its own code to the browser. Adding 'use client' at the top of a file opts that specific component (and its children) into the browser, where it can use hooks and event handlers.
127SSR vs SSG vs CSR — explain all three and when to use each.Intermediate+
Easy explanation
CSR (Client-Side Rendering) sends a mostly empty HTML shell, and JavaScript running in the browser fetches data and builds the actual page — great for highly interactive, logged-in dashboards where SEO doesn't matter, but slower for first paint since the browser has to download and run JS before anything meaningful appears. SSR (Server-Side Rendering) builds the full HTML on the SERVER for every request, sending a complete, already-populated page — better for SEO and faster first paint, at the cost of server work on every single request.
SSG (Static Site Generation) builds the HTML ONCE ahead of time (at build/deploy time), and every request afterward just serves that same pre-built file instantly from a CDN — ideal for content that doesn't change per-request, like a marketing page or blog post. Modern Next.js blurs these into a spectrum with per-route caching and revalidation, rather than forcing you to pick one globally for the whole app.
| CSR | SSR | SSG | |
|---|---|---|---|
| HTML built when? | In the browser, after JS runs | On the server, per request | Once, ahead of time at build |
| Best for | Interactive logged-in apps | Personalized or frequently-changing pages | Marketing pages, blogs, docs |
| SEO / first paint | Weaker | Strong | Strongest — instant, pre-built |
// Server Component fetching fresh data per request (SSR-like)
export default async function Page(){
const data = await fetch('https://api.example.com/data', { cache:'no-store' });
return <main>{await data.json().then(d=>d.title)}</main>;
}
128What is the App Router and how is it structured?Intermediate+
Easy explanation
The App Router is Next.js's file-system-based routing convention built on the `app` directory: a folder's path maps directly to a URL route, and special files within each folder have specific meaning — `page.tsx` defines the route's actual UI, `layout.tsx` defines shared UI that wraps that route and its children (persisting across navigations without re-rendering), and `loading.tsx`/`error.tsx` define automatic loading and error states for that segment.
This replaced the older `pages` directory approach, and its biggest architectural shift is that components in `app` are Server Components by default (rendered on the server, no JS shipped for them) unless you explicitly opt a file into client-side behavior with `'use client'` at the top.
app/
layout.tsx // shared shell (nav, footer) for everything below
page.tsx // route: '/'
loading.tsx // automatic loading UI for this segment
blog/
[slug]/page.tsx // route: '/blog/:slug'
129Server Components vs Client Components — what can and can't each one do?Advanced+
Easy explanation
Server Components run ONLY on the server and never ship their own JavaScript to the browser at all — they can directly access a database, read environment secrets, and do expensive work without that code or those secrets ever reaching the client bundle. Their trade-off: they cannot use hooks like useState/useEffect, cannot attach event handlers like onClick, and cannot use browser-only APIs — because none of that code exists in a browser at all.
Client Components (marked with `'use client'` at the top of the file) are the ones that actually get downloaded and run in the browser, and they're where you use interactivity — state, effects, click handlers, browser APIs like localStorage. The Next.js best practice: keep most of your tree as Server Components by default for performance and bundle size, and only mark the specific interactive leaf components as Client Components, rather than marking a whole page client-side out of habit.
// app/page.tsx — Server Component (default, no directive needed)
import Counter from './Counter';
export default async function Page(){
const posts = await db.post.findMany(); // safe: runs only on the server
return <><h1>Posts</h1><Counter/></>;
}
// app/Counter.tsx — Client Component
'use client';
import { useState } from 'react';
export default function Counter(){
const [n, setN] = useState(0);
return <button onClick={()=>setN(n+1)}>{n}</button>;
}
130What are Server Actions and how are they different from a traditional API route?Advanced+
Easy explanation
A Server Action is an async function marked with `'use server'` that can be called directly from a Client or Server Component — often right from a form's `action` attribute — without you manually creating a separate API route, writing a fetch call, and wiring up the request/response plumbing yourself. Next.js handles the network call under the hood as a special POST request.
The key benefit is co-location and simplicity: your mutation logic lives right next to the component that uses it, works even with JavaScript partially loaded (since forms can progressively enhance), and integrates directly with Next.js's caching/revalidation system (`revalidatePath`) to refresh stale data after a write — while a traditional API route still has its place for endpoints meant to be called by external clients, mobile apps, or webhooks, not just your own UI.
// app/actions.ts
'use server';
export async function createPost(formData: FormData){
const title = formData.get('title');
await db.post.create({ data: { title } });
revalidatePath('/posts'); // refresh cached data after the write
}
// used directly in a form: <form action={createPost}>
131What is Incremental Static Regeneration (ISR) / revalidation, and what problem does it solve?Advanced+
Easy explanation
Fully static pages (built once) are fast but can go stale if the underlying data changes — you'd otherwise need a full rebuild and redeploy to update them. ISR/revalidation lets a statically-rendered page automatically refresh its cached content after a time interval (time-based revalidation) or on-demand when specific data actually changes (via `revalidatePath`/`revalidateTag`), WITHOUT needing to rebuild and redeploy the entire site.
This gives you the performance of static generation (serving pre-built HTML instantly) combined with the freshness of dynamic rendering, which is why it's the default recommendation for content that changes occasionally but not on every single request — like a product page whose price updates a few times a day.
// time-based revalidation: refresh this fetch's cached result at most every 60s
fetch('https://api.example.com/products', { next: { revalidate: 60 } });
// on-demand: trigger a refresh immediately after a specific write, e.g. inside a Server Action
revalidatePath('/products');
132What are dynamic routes and catch-all routes in the App Router?Intermediate+
Easy explanation
A dynamic route uses square brackets in the folder name to capture a variable segment of the URL as a parameter — `app/products/[id]/page.tsx` matches `/products/42` and gives your page component access to `id: '42'`. This is how you build one reusable template that renders differently for every product, blog post, or user profile, instead of a separate file per item.
A catch-all route (`[...slug]`) captures MULTIPLE remaining URL segments as an array instead of just one — useful for something like a documentation site where `/docs/a/b/c` should all route through one flexible template. An optional catch-all (`[[...slug]]`) additionally matches the base route with zero segments too, like `/docs` itself.
app/products/[id]/page.tsx // matches /products/42
app/docs/[...slug]/page.tsx // matches /docs/a/b/c -> slug = ['a','b','c']
export default function Page({ params }){ return <h1>{params.id}</h1>; }
133What are Route Handlers and when would you still write one instead of a Server Action?Intermediate+
Easy explanation
A Route Handler is Next.js's way of defining an actual HTTP endpoint inside the App Router, using a `route.ts` file with exported functions named after HTTP methods (GET, POST, etc.), built on the standard Web Request/Response APIs rather than a Node-specific request object.
You'd reach for a Route Handler instead of a Server Action specifically when the endpoint needs to be called by something OTHER than your own React components — an external mobile app, a third-party webhook receiver, a public API for other developers, or anything that needs a real, stable, independently-documented URL rather than the internal mechanism Server Actions use.
// app/api/users/route.ts
export async function GET(){
const users = await db.user.findMany();
return Response.json(users);
}
export async function POST(req: Request){
const body = await req.json();
const user = await db.user.create({ data: body });
return Response.json(user, { status: 201 });
}
134What is hydration and what causes a 'hydration mismatch' error?Advanced+
Easy explanation
Hydration is the process where React 'attaches' its interactive behavior (event listeners, internal state) onto HTML that was already rendered by the server, rather than throwing that HTML away and rebuilding it from scratch in the browser — this is what makes server-rendered pages become interactive after loading.
A hydration mismatch happens when the HTML React renders on the CLIENT during hydration doesn't exactly match what the server actually sent — common causes include using `Date.now()`, `Math.random()`, or checking `typeof window` directly in render logic (all of which can produce different output on server vs. client), or browser extensions injecting markup into the DOM before React hydrates. The fix is to keep initial render output fully deterministic between server and client, and defer any genuinely client-only values to after the component has mounted (inside useEffect).
// causes a mismatch: server renders one timestamp, client hydrates with a different one
function Clock(){ return <span>{new Date().toLocaleTimeString()}</span>; }
// fix: only render the real value after mounting on the client
function Clock(){
const [time, setTime] = useState<string|null>(null);
useEffect(()=> setTime(new Date().toLocaleTimeString()), []);
return <span>{time ?? '--:--'}</span>;
}
135What is streaming with Suspense in Next.js, and why does it improve perceived performance?Advanced+
Easy explanation
Instead of waiting for EVERY piece of data on a page to be ready before sending any HTML at all, streaming lets the server send the parts of the page that are already ready immediately, and progressively stream in the slower parts as they finish — wrapping a slow section in a `<Suspense>` boundary with a fallback tells Next.js exactly where it's allowed to 'cut' the page and send the rest later.
This directly improves perceived performance: a user sees the fast, mostly-static shell of the page (navigation, layout) almost instantly, with a loading placeholder only around the specific slow part (like a personalized recommendations widget hitting a slow third-party API), instead of staring at a completely blank white page until literally everything is ready.
export default function Page(){
return (
<>
<Header /> {/* fast, sent immediately */}
<Suspense fallback={<Skeleton/>}>
<SlowRecommendations /> {/* streamed in once ready */}
</Suspense>
</>
);
}
136What is middleware in Next.js and what are its constraints?Advanced+
Easy explanation
Next.js middleware runs BEFORE a request reaches a route, letting you rewrite the URL, redirect, modify request/response headers, or run logic like checking an auth cookie — commonly used for things like redirecting unauthenticated users away from a protected route before any page code even runs.
An important constraint: middleware runs in a restricted 'Edge' runtime by default for performance reasons (so it can execute at CDN edge locations close to the user), which means it can't use full Node.js APIs the way a normal Route Handler can — heavier logic (like a full database query) generally belongs in the actual route/page instead, with middleware reserved for lightweight, fast checks and redirects.
// middleware.ts
export function middleware(req){
const hasSession = req.cookies.has('session');
if (!hasSession && req.nextUrl.pathname.startsWith('/dashboard')){
return NextResponse.redirect(new URL('/login', req.url));
}
}
export const config = { matcher: '/dashboard/:path*' };
137How does Next.js's built-in Image component improve performance over a plain <img> tag?Intermediate+
Easy explanation
The `next/image` component automatically handles several performance pitfalls that a plain `<img>` leaves entirely up to you: it serves appropriately-sized images for the actual device/viewport (instead of one large file downloaded on every device), automatically converts to modern efficient formats like WebP/AVIF when the browser supports them, and lazy-loads off-screen images by default.
Critically, it also requires you to specify width and height (or use `fill` with a sized parent), which lets the browser reserve the correct layout space BEFORE the image downloads — directly preventing the Cumulative Layout Shift problem covered in the CSS section, something a plain `<img>` without explicit dimensions doesn't protect you from automatically.
import Image from 'next/image';
<Image src="/hero.jpg" alt="Product hero" width={800} height={400} priority />
Git & GitHub
Git's Three Areas
Changes move through three stages before reaching a shared remote: your actual edited files, a staging snapshot you've chosen to include, and your local commit history — only 'push' shares that history with everyone else.
138What is Git and how is it different from GitHub?Beginner+
Easy explanation
Git is a distributed version-control SYSTEM — a command-line tool that tracks every change to your files over time, letting you branch, merge, and roll back history, entirely on your own machine with no internet connection required. Every developer's local clone contains the FULL history of the project, not just a partial copy.
GitHub is a HOSTING SERVICE built around Git — it adds a web interface, remote storage for repositories, collaboration features (pull requests, issues, code review), and CI/CD integration on top of plain Git. GitLab and Bitbucket are direct competitors offering similar hosting around the same underlying Git tool — Git itself doesn't require any of them to function.
git init # start tracking a project locally
git remote add origin <url> # connect it to a GitHub repository
git push -u origin main # share your local history to GitHub
139git fetch vs git pull — what's actually different?Beginner+
Easy explanation
git fetch downloads the latest commits/branches from the remote into your local repository's 'remote-tracking' branches, but does NOT touch your current working branch at all — it just updates Git's knowledge of what's on the remote, letting you inspect changes before deciding to integrate them.
git pull does a fetch AND THEN automatically merges (or rebases, if configured) those fetched changes into your current branch immediately. Many experienced developers prefer `fetch` followed by a manual, deliberate merge/rebase specifically so they can review incoming changes first, rather than letting `pull` blindly integrate them right away.
git fetch origin # see what's new, without changing your branch
git log origin/main # inspect what changed before merging
git pull origin main # fetch AND merge/rebase in one step
140merge vs rebase — what's the real difference in resulting history, and when should you avoid rebase?Intermediate+
Easy explanation
git merge combines two branches by creating a new 'merge commit' that has two parents, preserving the exact history of both branches as they actually happened, including every side branch's individual commits — the history is truthful but can look messy with many parallel lines. git rebase instead REWRITES your branch's commits so they appear to have been built on top of the latest version of the target branch, producing a clean, linear history with no merge commits at all.
The critical rule: never rebase commits that have already been PUSHED and that other people might have based their own work on — rebase rewrites commit hashes entirely, so anyone else who already has the old commits will get confusing, conflicting history when they try to sync. Rebase is safe and popular for cleaning up your OWN local, not-yet-shared commits before opening a pull request; merge is the safer default for integrating already-shared/public branches.
git checkout feature-branch
git rebase main # replays feature-branch's commits on top of the latest main — rewrites history, use only if not yet pushed/shared
141What is a merge conflict and how do you actually resolve one?Intermediate+
Easy explanation
A merge conflict happens when Git can't automatically combine two changes because they touched the SAME lines of the same file in incompatible ways — Git marks the conflicting section directly in the file with `<<<<<<<`, `=======`, and `>>>>>>>` markers showing both competing versions, and pauses the merge/rebase for you to resolve it manually.
To resolve: open the conflicted file, decide what the final correct code should be (keeping one side, the other, or a combination), remove ALL the conflict marker lines, save the file, then `git add` the resolved file to mark it as fixed, and finally run `git commit` (for a merge) or `git rebase --continue` (for a rebase) to finish the operation.
<<<<<<< HEAD
const greeting = 'Hello';
=======
const greeting = 'Hi there';
>>>>>>> feature-branch
// after manually picking the correct version and deleting the markers:
git add file.js
git commit
142What is .gitignore and what should typically go in it?Beginner+
Easy explanation
A .gitignore file lists file/folder patterns that Git should never track or offer to commit — things that are either regenerated automatically (build output, compiled files), machine-specific (editor config, OS files), too large or binary to usefully version, or SENSITIVE (like a `.env` file containing real API keys and database passwords).
Committing secrets to Git is a genuinely serious security mistake — even if you delete the file in a LATER commit, the secret still exists in the repository's history forever unless you rewrite history entirely (and even then, anyone who already cloned it has a copy) — which is why `.env` and similar files should be in `.gitignore` from the very first commit, not added after the fact.
node_modules/
.env
.next/
dist/
*.log
.DS_Store
143What is a pull request and what does a good PR review workflow look like?Intermediate+
Easy explanation
A pull request (PR) is a request to merge changes from one branch into another, opened on a hosting platform like GitHub, that serves as the central place for reviewing a proposed change before it becomes part of the main codebase — reviewers can comment on specific lines, request changes, approve, and see automated checks (tests, linting, builds) run against the change.
A healthy workflow: keep PRs small and focused on one logical change (easier and faster to review thoroughly than a giant PR touching 50 files), write a clear description of WHAT changed and WHY, ensure CI checks pass before requesting review, and treat review feedback as a normal, expected part of shipping quality code rather than something to rush past.
# typical flow
git checkout -b feature/add-search
# ...make changes, commit...
git push -u origin feature/add-search
# open a PR on GitHub targeting 'main', wait for CI + review, then merge
144What is git cherry-pick and when would you actually use it?Advanced+
Easy explanation
git cherry-pick takes ONE specific commit from anywhere in the repository's history and reapplies just that single change on top of your CURRENT branch, creating a brand new commit with the same changes (but a different commit hash, since it's technically a new commit applied in a new context).
A common real use case: you fixed a critical bug on your feature branch, but that fix needs to also go out immediately on the production hotfix branch without waiting for the rest of your unfinished feature — cherry-pick lets you grab JUST that bug-fix commit and apply it to the hotfix branch, without bringing along your feature's other unfinished, unrelated commits.
git log feature-branch # find the commit hash of the fix, e.g. abc1234
git checkout hotfix-branch
git cherry-pick abc1234 # applies just that one commit here
145What is git bisect and how does it help find the commit that introduced a bug?Advanced+
Easy explanation
git bisect performs an automated BINARY SEARCH through your commit history to pinpoint the exact commit that introduced a regression, rather than manually checking out and testing commits one by one. You tell it one commit you know is 'good' (bug didn't exist) and one that's 'bad' (bug exists), and it checks out the commit exactly halfway between them for you to test.
You then mark that midpoint as `good` or `bad` based on whether the bug is present, and bisect automatically narrows the search range by half again, repeating until it's isolated the single specific commit responsible — turning what could be a search through hundreds of commits into roughly log2(N) test cycles, which is dramatically faster for a large history.
git bisect start
git bisect bad # current commit has the bug
git bisect good v1.0.0 # this old tag was known-good
# Git checks out the midpoint; you test it, then:
git bisect good # or: git bisect bad
# repeat until Git reports the exact culprit commit
146What is the difference between git reset, git revert, and git checkout for undoing changes?Advanced+
Easy explanation
git revert creates a NEW commit that undoes the changes of a previous commit, while keeping the original commit fully intact in history — this is the SAFE choice for undoing something that's already been pushed and shared, since it doesn't rewrite existing history that others may have already based work on. git reset actually MOVES your branch pointer backward (optionally also changing your staged/working files), effectively rewriting history — safe for undoing local, not-yet-pushed commits, but dangerous on shared branches for the same reason as rebase.
git checkout (or the newer `git restore`/`git switch` split) is used to discard uncommitted changes in a specific file back to its last committed version, or to switch between branches/commits — it doesn't rewrite committed history at all, it just changes what's currently checked out in your working directory.
| Command | Rewrites history? | Safe on already-pushed/shared commits? |
|---|---|---|
| git revert | No — adds a new undo commit | Yes |
| git reset | Yes — moves the branch pointer back | No — avoid on shared branches |
| git checkout/restore (a file) | No — just discards uncommitted local changes | N/A — doesn't touch commit history |
git revert abc1234 # safe: adds a new commit undoing abc1234, keeps history intact
git reset --hard HEAD~1 # dangerous on shared branches: actually erases the last local commit
147What is a Git tag and how is it different from a branch?Intermediate+
Easy explanation
A branch is a MOVABLE pointer to a commit — it's designed to advance forward automatically as you make new commits on it, representing ongoing work. A tag is typically a PERMANENT, unmoving marker on one specific commit, used to mark a meaningful point in history that shouldn't change — most commonly a released version number.
Annotated tags (created with `-a`) also store extra metadata (a message, the tagger's name, a timestamp) and can be cryptographically signed, unlike lightweight tags which are just a simple named pointer — annotated tags are generally the recommended choice for anything representing an actual release.
git tag -a v1.2.0 -m "Release 1.2.0: adds search feature"
git push origin v1.2.0
Docker, CI/CD & Deployment
A Typical CI/CD Pipeline
A reliable pipeline rejects a broken change (failed tests) before it ever gets close to production, and only routes real traffic to a new deployment once it's confirmed healthy — giving you a safe rollback point at every stage.
148What is Docker and what problem does it actually solve?Intermediate+
Easy explanation
Docker packages an application together with everything it needs to run — its code, runtime, system libraries, and configuration — into a single portable unit called a container IMAGE. The core problem it solves is 'it works on my machine' — differences in OS, installed library versions, or environment configuration between a developer's laptop, a CI server, and production have historically caused bugs that only appear in one environment.
Because a container includes its own isolated filesystem and dependencies, the exact same image runs identically across your laptop, a CI runner, and a production server — you're no longer relying on every environment happening to have the right Node/Python version and system libraries pre-installed and matching.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
149What's the difference between a Docker image and a container?Intermediate+
Easy explanation
An image is an immutable, read-only TEMPLATE — a layered snapshot of a filesystem plus metadata (like what command to run) built from a Dockerfile. It doesn't run anything by itself; it's just a packaged blueprint that can be shared, versioned, and stored in a registry.
A container is a RUNNING (or stopped) INSTANCE created from an image — the same way a class and an object relate in object-oriented programming. You can start many independent containers from the exact same image simultaneously, each with its own isolated running process, memory, and writable filesystem layer on top of the shared read-only image beneath it.
docker build -t myapp:1.0 . # build the image (the template)
docker run -p 3000:3000 myapp:1.0 # start a container (a running instance)
docker ps # list currently running containers
150What is a multi-stage Docker build and why does it produce a smaller final image?Advanced+
Easy explanation
A multi-stage build uses MULTIPLE `FROM` statements in one Dockerfile — an early stage does the heavy lifting (installing full dev dependencies, compiling TypeScript, bundling assets) using a large base image, and a LATER, final stage starts fresh from a minimal base image and copies ONLY the compiled output/production artifacts from the earlier stage, discarding everything else.
This matters because dev dependencies, build tools, and source files that were only needed to PRODUCE the app add real size and potential attack surface if they end up in your final production image unnecessarily — a multi-stage build keeps the shipped image lean, containing only what's actually needed to RUN the app, not build it.
# Stage 1: build
FROM node:22 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Stage 2: final, minimal runtime image
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
151What is Docker Compose and when do you need it?Intermediate+
Easy explanation
Real applications rarely run as a single container in isolation — a typical setup needs an app server, a database, maybe a cache, running together and able to talk to each other. Docker Compose lets you describe this whole multi-container STACK in one YAML file — each service, its image/build source, ports, environment variables, and dependencies — and start/stop the entire stack with a single command.
It's especially valuable for local development, letting every developer on a team spin up an identical, fully-configured environment (app + Postgres + Redis, say) with one command, instead of everyone manually installing and configuring those services individually on their own machine and inevitably drifting out of sync.
# docker-compose.yml
services:
app:
build: .
ports: ['3000:3000']
depends_on: [db]
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: devpassword
152What is CI/CD and what's the difference between continuous delivery and continuous deployment?Intermediate+
Easy explanation
Continuous Integration (CI) means every code change is automatically built and tested (linting, unit tests, integration tests) as soon as it's pushed, catching problems immediately rather than discovering them much later when many changes have piled up together. This is the foundation both delivery and deployment build on top of.
Continuous Delivery means every change that passes CI is automatically prepared into a deployable, release-ready artifact, but a human still explicitly approves/triggers the actual release to production. Continuous Deployment goes one step further and removes that manual approval gate entirely — every change that passes all automated checks is deployed to production automatically, with no human in the loop at all. The distinction matters in interviews because people frequently use 'CD' to mean either one without clarifying which.
push -> lint -> unit tests -> integration tests -> build artifact -> (manual approval?) -> deploy
153What does a reverse proxy do, and why do most production setups put one in front of the app server?Advanced+
Easy explanation
A reverse proxy (like Nginx, Caddy, or a managed load balancer) sits BETWEEN the internet and your actual application server(s), receiving all incoming traffic first and forwarding it onward. It commonly handles TLS/HTTPS termination (decrypting incoming HTTPS so your app server only deals with plain HTTP internally), gzip/brotli compression, request routing to different backend services, and serving static files directly without ever bothering your application process.
Using a reverse proxy also lets you horizontally scale — it can load-balance incoming requests across multiple identical application instances, hide the internal architecture (clients only ever see the proxy's address, not individual server IPs), and centralize concerns like rate limiting or basic security headers in one place instead of duplicating that logic inside every backend service.
# simplified Nginx reverse proxy config
server {
listen 443 ssl;
location / {
proxy_pass http://app_servers; # forwards to one of several app instances
}
}
154Horizontal vs vertical scaling — what are the trade-offs?Advanced+
Easy explanation
Vertical scaling means giving a SINGLE server more resources — more CPU, more RAM, a faster disk. It's simple (no architectural changes needed) but has a hard ceiling (there's a biggest machine you can rent) and creates a single point of failure — if that one bigger machine goes down, everything goes down with it.
Horizontal scaling means running MULTIPLE instances of your application across several machines/containers, and distributing load across them (usually with a load balancer). This has no practical ceiling and improves fault tolerance (one instance failing doesn't take down the whole service), but it requires your application to be effectively STATELESS — any instance must be able to handle any request, which means session data, file uploads, and similar per-request state need to live in a shared external store (a database, Redis, object storage) rather than in that one server's local memory or disk.
| Vertical scaling | Horizontal scaling | |
|---|---|---|
| How it scales | Bigger single machine | More machines/instances |
| Ceiling | Limited — biggest available machine | Effectively unlimited |
| Requires stateless app design? | No | Yes |
| Single point of failure? | Yes | No, if done correctly |
# horizontal scaling requires shared state, not in-memory per-instance state:
// BAD: session stored in this specific server's memory
// GOOD: session stored in Redis, reachable by every instance
155What is zero-downtime deployment and how does a rolling deployment achieve it?Advanced+
Easy explanation
Zero-downtime deployment means users never experience an outage or dropped requests during a release, even though the underlying application code is being replaced. The naive approach — stop the old version, then start the new one — inevitably creates a gap where no server is available to handle requests at all.
A rolling deployment avoids this by starting new instances running the NEW version FIRST, waiting for them to pass health checks confirming they're actually ready to serve traffic, THEN gradually shifting live traffic over to them, and only stopping the OLD instances after the new ones are confirmed healthy and handling load — at every point during the transition, there's always at least one healthy instance (old or new) available to serve requests.
deploy new version instances -> wait for health check to pass ->
shift load balancer traffic to new instances -> drain + stop old instances
156What is a health check endpoint and why do orchestration platforms rely on it?Intermediate+
Easy explanation
A health check is a simple endpoint (commonly `/health` or `/healthz`) that an application exposes purely so external systems — a load balancer, a container orchestrator like Kubernetes, an uptime monitor — can automatically verify the application is actually running correctly and ready to receive real traffic, without needing a human to check manually.
A GOOD health check does more than just return 200 OK unconditionally — it should verify the things the app actually depends on to function (like confirming it can reach its database connection pool), so that an instance which is technically running but can't reach its database gets correctly marked unhealthy and taken out of rotation, instead of continuing to receive traffic it can't actually serve.
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1'); // confirms the DB connection actually works
res.status(200).json({ status: 'ok' });
} catch {
res.status(503).json({ status: 'unhealthy' });
}
});
157How should secrets (API keys, database passwords) be managed in a deployment pipeline?Advanced+
Easy explanation
Secrets should never be committed to source control (even in a private repository — access controls change, repos get accidentally made public, and the secret remains in Git history forever regardless), and should never be hardcoded directly in a Dockerfile or baked into a built image, since anyone who can pull that image can extract the secret from its layers.
The standard approach: store secrets in your deployment platform's dedicated secret management system (environment variables injected at deploy time, or a dedicated secrets manager like AWS Secrets Manager/HashiCorp Vault), inject them into the running container as environment variables at STARTUP rather than build time, restrict which environments/services can access which secrets, and rotate them periodically — especially immediately after any suspected exposure.
# Dockerfile — never do this:
# ENV DATABASE_PASSWORD=hardcoded_secret_here
# Instead, injected at deploy time by the platform:
# docker run -e DATABASE_URL=$SECRET_FROM_VAULT myapp
GraphQL & Realtime
REST vs GraphQL: Fetching a Post with its Author and Comments
REST typically needs one endpoint per resource, so a nested view often requires several round trips. GraphQL exposes one endpoint where the client specifies exactly which fields and relationships it needs, resolved server-side in a single request.
158What is GraphQL and how is its request model fundamentally different from REST?Intermediate+
Easy explanation
GraphQL is a query language and execution engine for APIs where the CLIENT specifies exactly which fields and nested relationships it wants back, all in a single request to one endpoint — instead of REST's model of many separate endpoints, each returning a fixed, predetermined shape of data regardless of whether the client needs all of it.
This solves two common REST pain points directly: OVER-fetching (getting back a bunch of fields you don't actually need, wasting bandwidth) and UNDER-fetching (needing multiple separate round trips to assemble one nested view, like a post plus its author plus its comments). The trade-off is that GraphQL servers are more complex to build correctly (see the N+1 problem below) and typically don't benefit from simple HTTP caching the way REST's distinct URLs naturally do.
query {
post(id: 1) {
title
author { name }
comments { text }
}
}
159Query vs Mutation in GraphQL — what's the difference?Intermediate+
Easy explanation
A Query represents a READ operation — fetching data without causing any side effects on the server, conceptually similar to an HTTP GET. Queries can be executed in parallel by the GraphQL engine since they're not expected to change anything.
A Mutation represents an operation that CHANGES server-side state — creating, updating, or deleting data, similar to a REST POST/PUT/PATCH/DELETE. The GraphQL spec guarantees multiple mutations in a single request execute sequentially (one completes before the next starts), specifically because side-effecting operations often need to happen in a predictable order, unlike queries.
mutation {
createPost(title: "Hello World") {
id
title
}
}
160What is a resolver in GraphQL?Intermediate+
Easy explanation
A resolver is the actual function responsible for returning the data for ONE SPECIFIC FIELD in your GraphQL schema — when a query asks for `post.author.name`, GraphQL calls the resolver for `post`, then calls the resolver for `author` on that result, then the resolver for `name` on THAT result, walking down the query field by field.
Each resolver typically knows how to fetch just its own small piece of data — often from a database, another service, or a computed value — and doesn't need to know about the rest of the query at all, which is what makes the system composable: you define how to resolve each field once, and clients can combine them into arbitrarily nested queries.
const resolvers = {
Query: {
post: (_, { id }) => db.post.findById(id),
},
Post: {
author: (post) => db.user.findById(post.authorId), // resolves the nested 'author' field
},
};
161What is the N+1 problem in GraphQL and how does DataLoader solve it?Advanced+
Easy explanation
The N+1 problem happens when resolving a LIST of items, and each item's nested field triggers its OWN separate database query — fetching 20 posts plus each post's author naively results in 1 query for the posts, PLUS 20 more individual queries (one per post) for each author, when a single batched query could have fetched all 20 authors at once.
DataLoader solves this by BATCHING and CACHING requests within a single tick of execution — instead of each resolver immediately firing its own query, DataLoader collects all the individual 'give me this author's ID' requests that occur during that request, then fires ONE combined query for all of them together (e.g. `WHERE id IN (1,2,3,...)`), and hands each resolver back its specific result — cutting 21 queries down to 2.
const userLoader = new DataLoader(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: ids } } }); // one batched query
return ids.map(id => users.find(u => u.id === id));
});
// resolver:
author: (post) => userLoader.load(post.authorId) // batched automatically across the whole request
162What is a WebSocket and how is it fundamentally different from a normal HTTP request?Intermediate+
Easy explanation
A normal HTTP request is one-shot and client-initiated: the client sends a request, the server sends back exactly one response, and the connection is done (or reused for the next separate request/response pair). A WebSocket establishes a single PERSISTENT connection that stays open, over which EITHER side can send messages to the other AT ANY TIME, without needing to wait for a 'request' first.
This full-duplex, always-open nature is exactly what makes WebSockets suitable for genuinely real-time use cases — a chat app, live collaborative editing, live sports scores — where the server needs to PUSH new data to the client the instant it happens, rather than the client having to repeatedly ask 'anything new yet?' (polling).
const socket = new WebSocket('wss://example.com/chat');
socket.onmessage = (event) => console.log('received:', event.data);
socket.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
163What is Socket.IO and how is it different from the raw WebSocket API?Intermediate+
Easy explanation
Socket.IO is a library built ON TOP of WebSockets (and other transports as a fallback) that adds features the raw WebSocket API doesn't provide out of the box: automatic reconnection if a connection drops, a room/namespace system for grouping connected clients (like 'everyone in chat room #42'), acknowledgements (confirming a message was actually received), and automatic fallback to HTTP long-polling for environments where WebSockets are blocked.
An important nuance: Socket.IO is NOT the same protocol as raw WebSockets — a plain WebSocket client can't connect to a Socket.IO server and vice versa, because Socket.IO adds its own framing and handshake on top. Choosing Socket.IO trades a bit of protocol lock-in for a lot of practical convenience most real-time apps end up needing anyway.
io.on('connection', (socket) => {
socket.join('room-42');
socket.on('chat message', (msg) => {
io.to('room-42').emit('chat message', msg); // only broadcasts to that room
});
});
164How do realtime systems scale across multiple server instances?Advanced+
Easy explanation
A single server instance can easily broadcast a message to all the WebSocket clients connected directly TO IT. But once you scale horizontally to multiple instances behind a load balancer, a client connected to Server A has no direct way to receive a message triggered by something happening on Server B — each instance only knows about its own local connections.
The standard fix is a shared PUB/SUB layer (commonly Redis) that all instances subscribe to: when any instance needs to broadcast an event, it publishes it to Redis, and every instance (including ones with no directly relevant connections) receives that published message and forwards it to whichever of ITS OWN locally-connected clients actually care about it — effectively using Redis as a message bus connecting otherwise-isolated server instances.
// each app instance subscribes to the same Redis channel
redisSub.subscribe('chat-events');
redisSub.on('message', (channel, message) => {
io.emit('chat message', JSON.parse(message)); // forward to THIS instance's local clients
});
// publishing from any instance reaches every instance:
redisPub.publish('chat-events', JSON.stringify(newMessage));
165What is Server-Sent Events (SSE) and how does it compare to WebSockets for realtime updates?Advanced+
Easy explanation
SSE lets a server push a continuous stream of text-based events to the client over a single, long-lived HTTP connection — but unlike WebSockets, it's ONE-DIRECTIONAL: only the server can push data to the client; the client can't send messages back over that same connection (it would need a separate normal HTTP request for that).
SSE is a genuinely good fit for use cases that only need server-to-client push and nothing more complex — live notifications, a live-updating dashboard, streaming AI-generated text token by token — because it's simpler to implement and works over plain HTTP (no separate protocol upgrade, easier to work with existing HTTP infrastructure like proxies and load balancers) than a full WebSocket. Choose WebSockets specifically when you need genuine two-way, low-latency communication like a chat app or collaborative editor.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Direction | Server → client only | Both directions |
| Protocol | Plain HTTP | Separate ws:// protocol (upgraded from HTTP) |
| Best for | Notifications, live feeds, streaming text | Chat, collaborative editing, gaming |
// server
res.setHeader('Content-Type', 'text/event-stream');
res.write(`data: ${JSON.stringify({ progress: 50 })}\n\n`);
// client
const events = new EventSource('/api/progress');
events.onmessage = (e) => console.log(JSON.parse(e.data));
Redis & Caching
The Cache-Aside Pattern
On a cache hit, the database is never touched at all. On a miss, the app falls back to the database, then populates the cache so the NEXT request for the same data is fast — this is why the first request after a cache expires is always the slowest.
166What is Redis and what makes it different from a normal database?Intermediate+
Easy explanation
Redis is an in-memory data store — it keeps its entire dataset in RAM rather than on disk (though it can optionally persist to disk for durability), which makes reads and writes extremely fast compared to a traditional disk-backed database. It supports several data structures beyond simple key-value pairs — strings, hashes, lists, sets, sorted sets — each suited to different use cases.
Because of this speed, Redis is most commonly used as a CACHE sitting in front of a slower primary database, but it's also genuinely used as a primary store for things that are naturally transient or speed-critical: session storage, rate-limiting counters, real-time leaderboards (via sorted sets), job queues, and pub/sub messaging between services.
SET user:1 "Tanveer" EX 300 # store a value, expires automatically after 300 seconds
GET user:1
167What is the cache-aside pattern and what's the downside of the first request after data expires?Intermediate+
Easy explanation
Cache-aside (a.k.a. lazy loading) means the APPLICATION itself is responsible for managing the cache: on a read, check the cache first; if the data is there (a 'hit'), return it immediately without touching the database at all; if it's not there (a 'miss'), fall back to querying the database, then store that result in the cache before returning it, so future reads are fast.
The clear downside: the very first request after a cache entry expires (or was never cached) always pays the full cost of hitting the slower database — there's no way around that with pure cache-aside. If MANY requests for that same now-expired key arrive simultaneously, they can all miss the cache at once and hammer the database together — this specific failure mode is called a 'cache stampede', covered separately below.
async function getUser(id){
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached); // hit — database never touched
const user = await db.user.findById(id); // miss — fall back
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300);
return user;
}
168What is TTL and why should almost every cache key have one?Intermediate+
Easy explanation
TTL (time-to-live) is the duration after which Redis automatically deletes a key on its own, without you needing to explicitly remove it. This bounds two things at once: how STALE the cached data can possibly get before it's forced to refresh, and how much MEMORY Redis uses over time, since old unused keys eventually clean themselves up automatically.
Choosing the right TTL is a real trade-off, not a default you can ignore: too short, and you lose most of the caching benefit because data keeps expiring and forcing database hits; too long, and users can see meaningfully stale data for longer than acceptable for that specific piece of information — the right value genuinely depends on how often the underlying data changes and how tolerant your use case is of staleness.
SETEX session:abc123 3600 "session-data" # expires automatically after 1 hour
169What is cache invalidation and why is it considered one of the genuinely hard problems in computing?Advanced+
Easy explanation
Cache invalidation is the process of removing or refreshing a cached value the moment its underlying source-of-truth data actually changes, so the cache doesn't keep serving stale data indefinitely. The famous quip ('there are only two hard problems in computer science: cache invalidation and naming things') exists because it's genuinely easy to get wrong in non-obvious ways — you have to correctly identify every code path that changes the underlying data and remember to invalidate every relevant cache key for it, and missing even one path leaves quietly stale data lingering.
Common strategies: explicit invalidation (actively deleting the specific cache key right when the underlying data is updated, in the same code path as the write), relying purely on TTL expiry as a safety net (simpler, but accepts some window of staleness), or a hybrid of both — explicit invalidation for correctness plus a TTL as a backstop in case an invalidation call is ever missed.
async function updateUser(id, data){
await db.user.update(id, data);
await redis.del(`user:${id}`); // explicitly invalidate so the next read gets fresh data
}
170What is a cache stampede and how do you prevent one?Advanced+
Easy explanation
A cache stampede (a.k.a. thundering herd) happens when a popular cache key expires, and then MANY concurrent requests for that same key all miss the cache AT THE SAME TIME, each independently falling through to hit the database simultaneously — instead of one request refreshing the cache, the database suddenly gets hit with a burst of duplicate, redundant load exactly at the moment the cache should have protected it.
Common defenses: request coalescing/locking (the first request to miss acquires a lock and fetches from the database while others wait briefly for that same result instead of each querying independently), jittered TTLs (randomizing expiry times slightly so many keys don't all expire at the exact same instant), and stale-while-revalidate (continuing to serve the slightly-stale cached value while ONE request refreshes it in the background, rather than every request blocking on a fresh fetch).
// simplified lock-based coalescing: only one request refreshes, others reuse its result
async function getWithLock(key, fetchFn){
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const gotLock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 5);
if (!gotLock) { await sleep(50); return getWithLock(key, fetchFn); } // wait and retry
const fresh = await fetchFn();
await redis.set(key, JSON.stringify(fresh), 'EX', 300);
return fresh;
}
171What is Redis pub/sub and what's its main limitation compared to a real message queue?Advanced+
Easy explanation
Redis pub/sub lets one client PUBLISH a message to a named channel, and every currently-SUBSCRIBED client instantly receives it — useful for real-time fan-out scenarios like broadcasting a chat message or a live event to multiple connected server instances (as covered in the Realtime section).
Its major limitation: Redis pub/sub messages are NOT durable or queued — if a subscriber isn't actively connected and listening at the exact moment a message is published, that message is simply lost forever for that subscriber; there's no persistence, replay, or guaranteed delivery. A real message queue (like RabbitMQ, or Redis's own separate Streams data structure) persists messages until they're explicitly acknowledged as processed, which is what you need whenever guaranteed delivery actually matters, not just best-effort real-time fan-out.
PUBLISH chat-room-1 "Hello everyone"
# any client NOT actively subscribed at this exact moment misses this message permanently
Architecture, Performance & Testing
A Typical Production Architecture
Each layer solves one specific problem: the CDN serves static content close to users, the load balancer spreads traffic across app instances, Redis absorbs repeated reads, and workers handle slow tasks off the request path. Each added layer is also added operational complexity — don't add one until you actually need it.
172Monolith vs microservices — what are the real trade-offs, and when should a team NOT start with microservices?Advanced+
Easy explanation
A monolith keeps the entire application as one deployable unit and one codebase — simpler to develop, test, and deploy locally, with function calls between modules instead of network calls, and no distributed-systems complexity to manage. Microservices split the application into independently deployable services, each owning its own data, communicating over the network — this allows independent scaling and independent deployment of each service, and lets different teams own different services.
The honest trade-off interviewers want to hear: microservices trade simplicity for FLEXIBILITY, and that flexibility comes at a real cost — network latency and failure handling between services, distributed data consistency challenges (no more simple database transactions across service boundaries), and significantly more operational complexity (service discovery, distributed tracing, versioning APIs between services). Most experienced engineers recommend starting with a well-structured MODULAR monolith and only splitting out microservices once you have a proven, specific need (a team boundary problem, or a genuinely different scaling requirement for one part of the system) — not by default, and not because it's currently fashionable.
| Monolith | Microservices | |
|---|---|---|
| Deployment | One unit | Many independent units |
| Cross-module calls | In-process function calls | Network calls (latency, can fail) |
| Data consistency | Simple — one database, real transactions | Hard — data is split across services |
| Best for | Most teams, especially early on | Proven scaling or team-boundary needs |
// modular monolith: separate modules, one deployable, one database
browser -> API layer -> [orders module | users module | billing module] -> one database
173What is a load balancer and what algorithms does it use to distribute traffic?Advanced+
Easy explanation
A load balancer sits in front of multiple identical application instances and distributes incoming requests across them, preventing any single instance from being overwhelmed while others sit idle, and enabling horizontal scaling in the first place. It typically also performs health checks, automatically removing unhealthy instances from rotation.
Common distribution algorithms: round-robin (requests cycle evenly through instances in order — simple, works well when requests are roughly equal cost), least-connections (send the next request to whichever instance currently has the fewest active connections — better when request processing times vary a lot), and IP-hash/sticky sessions (consistently route the same client to the same instance — sometimes needed when session state is kept in one server's memory rather than a shared store, though this is generally discouraged in favor of stateless instances).
client -> load balancer -> [instance A | instance B | instance C]
// least-connections example: sends to whichever instance is currently least busy
174What is a CDN and why does it improve performance globally?Intermediate+
Easy explanation
A CDN (Content Delivery Network) caches your static content (images, CSS, JS, sometimes whole HTML pages) across many geographically distributed 'edge' servers around the world, rather than every user's request travelling all the way back to your one origin server, wherever it happens to be located.
When a user requests a file, they're served from the CDN edge location physically CLOSEST to them, dramatically reducing network latency (the physical time it takes data to travel), and simultaneously reducing load on your actual origin server since it doesn't have to personally serve every single request for static assets, only the first request that populates each edge cache.
user in Tokyo -> nearby Tokyo CDN edge (cached, fast) -> only falls back to origin (e.g. in US) on a cache miss
175What are Core Web Vitals and what does each one actually measure?Advanced+
Easy explanation
Core Web Vitals are a specific set of metrics Google uses to quantify real user-experienced page quality, focusing on three dimensions: loading, interactivity, and visual stability. LCP (Largest Contentful Paint) measures how long it takes for the largest, most prominent piece of content (often a hero image or main heading) to become visible — this approximates 'when does the page feel loaded' to a real user.
INP (Interaction to Next Paint) measures the responsiveness of the page to user interactions throughout the ENTIRE visit (not just one interaction), replacing the older First Input Delay metric — a page can feel 'laggy' if clicks/taps take too long to visibly respond, even after it's fully loaded. CLS (Cumulative Layout Shift), covered in the CSS section, measures how much visible content unexpectedly jumps around during the page's lifetime. Good scores on all three are increasingly tied to search ranking, making them a real business concern, not just a technical nicety.
// Measure with real user data (field data) via tools like Chrome UX Report,
// combined with lab tools like Lighthouse for controlled, repeatable diagnostics.
176How do you actually reduce frontend JavaScript bundle size?Advanced+
Easy explanation
Start by measuring, not guessing — a bundle analyzer visualization shows exactly which dependencies are taking up the most space, since intuition about 'what's probably big' is frequently wrong. Common concrete fixes: code-split by route so users only download the JavaScript for the page they're actually viewing, lazy-load below-the-fold or rarely-used features (like a rich text editor or a chart library only needed on one settings page), and audit dependencies for smaller alternatives to bloated libraries you're only using 5% of.
Also critical in frameworks like Next.js: make sure server-only code and secrets (a large ORM, server-side validation logic) never accidentally end up bundled INTO the client-side JavaScript at all — this is both a bundle-size issue and a genuine security concern if secrets leak into a publicly downloadable bundle.
const Editor = React.lazy(() => import('./RichTextEditor')); // only downloaded when actually rendered
// Then measure the real impact:
// npx vite-bundle-visualizer (or webpack-bundle-analyzer)
177What is observability, and how do logs, metrics, and traces each play a different role?Advanced+
Easy explanation
Observability is the ability to understand what's actually happening INSIDE a running system from the outside, based on the data it emits — critical for diagnosing production issues you didn't specifically anticipate ahead of time. It's usually broken into three complementary pillars, each answering a different question.
Logs answer 'what specific events happened, and what were the details' — granular, often per-request or per-error text records. Metrics answer 'how is the system doing in aggregate, over time' — numeric time-series data like request rate, error rate, or CPU usage, good for dashboards and alerting on trends. Traces answer 'where did the time actually go for THIS ONE specific request', following it across every service and function it touched — essential in a distributed/microservices system where a single slow request might span five different services and you need to pinpoint exactly which one was the bottleneck.
request arrives -> traceId generated -> [API span] -> [auth span] -> [DB query span] -> [external API span]
// a trace ties all these spans together, showing exactly where the request spent its time
178Unit vs integration vs end-to-end (E2E) tests — what does each actually verify, and what's the 'testing pyramid'?Intermediate+
Easy explanation
Unit tests verify a single, small, isolated piece of logic (one function, one component) in complete isolation from its real dependencies (often using mocks/stubs for anything external) — they're fast to run and pinpoint exactly what broke, but don't prove the pieces actually work correctly TOGETHER. Integration tests verify that several real pieces work correctly when combined — like a route handler actually talking to a real (often test) database — catching bugs that only appear at the boundaries between components, which unit tests with mocks would miss entirely.
End-to-end tests drive the ACTUAL running application through a real browser (or equivalent), simulating a genuine user flow from start to finish — the strongest confidence that the whole system truly works, but also the slowest and most brittle (a small UI change can break a test that isn't really testing that specific UI detail). The 'testing pyramid' is the general guideline to write MANY fast unit tests, a moderate number of integration tests, and only a SMALL number of critical-path E2E tests — because E2E tests are valuable but too slow and flaky to be your primary safety net.
// unit test
expect(add(2,3)).toBe(5);
// integration test
const res = await request(app).post('/users').send({email:'a@b.com'});
expect(res.status).toBe(201);
// E2E test (e.g. with Playwright)
await page.goto('/signup'); await page.fill('#email','a@b.com'); await page.click('#submit');
await expect(page.locator('.welcome')).toBeVisible();
179What is graceful shutdown, and how does it interact with load balancer health checks during a deploy?Advanced+
Easy explanation
Covered briefly in the Node.js section — but the full picture involves the load balancer too. On receiving a shutdown signal (SIGTERM), a well-behaved service should: immediately start FAILING its health check (so the load balancer stops routing NEW traffic to it), stop accepting brand-new connections, but continue serving requests that are ALREADY in-flight until they finish (usually with a maximum timeout as a safety net).
Only after in-flight requests have finished (or the timeout is hit) should the process actually close its database connections and exit. If a service exits immediately on SIGTERM without this drain period, users with an in-flight request at that exact moment get an abrupt connection error — a very common, avoidable cause of brief error spikes during otherwise well-designed deployments.
let shuttingDown = false;
app.get('/health', (req,res)=> res.status(shuttingDown ? 503 : 200).end());
process.on('SIGTERM', async () => {
shuttingDown = true; // load balancer stops sending new traffic almost immediately
server.close(() => process.exit(0)); // finishes in-flight requests, then exits
});
Full-Stack Practical Scenarios
Anatomy of a Well-Designed Feature
Almost every practical scenario question is really testing the same instinct: never trust the client, always re-check authorization server-side, and make sure concurrent writes can't corrupt your data.
180Design a secure login flow, end to end.Advanced+
Easy explanation
Full flow: the client submits credentials over HTTPS only. The server first does basic validation (correct email format, password present), then looks up the user and verifies the submitted password against the stored HASH (never a plaintext comparison) using a slow, salted algorithm like Argon2id. On success, the server establishes a session — either a server-side session with an opaque cookie, or a signed token — setting HttpOnly, Secure and SameSite flags on any cookie used.
Just as important as the login itself: rate-limit login attempts per account/IP to slow down brute-force guessing, use a generic error message ('invalid email or password', not 'no such email' vs 'wrong password' separately, which would let an attacker enumerate valid accounts), and re-check authorization on EVERY subsequent protected request — the login flow only establishes identity once, it doesn't grant a free pass for every future request.
POST /login
-> validate input shape
-> find user by email
-> argon2.verify(storedHash, submittedPassword)
-> on success: create session, Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
-> on failure: generic 401, log the attempt for rate limiting
181Design an image/file upload feature for a web app.Advanced+
Easy explanation
A robust upload flow: the client requests a short-lived, pre-signed upload URL from your server (rather than uploading through your own app server directly, which would tie up your server's resources for potentially large files) — the server first confirms the user is authenticated and authorized to upload here, and validates expected file type/size limits before issuing that signed URL. The client then uploads DIRECTLY to object storage (like S3) using that signed URL.
After a successful upload, the client notifies your server (or your server is notified via a storage-provider webhook), which then stores the file's METADATA (owner, filename, size, storage path) in your regular database, and any heavier processing — generating thumbnails, virus scanning, transcoding video — happens asynchronously in a background job rather than blocking the upload response, with the file served afterward through a CDN for fast repeated access.
1. POST /uploads/sign (auth + validate type/size) -> returns a signed upload URL
2. Client uploads the file directly to object storage using that URL
3. POST /uploads/complete -> server stores metadata in DB
4. Background worker: generate thumbnail, run malware scan, etc.
182Design infinite scrolling for a long feed.Advanced+
Easy explanation
On the backend, use CURSOR-based pagination (covered in the Node/Express section) rather than offset pagination, since it stays correct and fast even as new items are added or removed while the user is scrolling. Each response includes the actual page of items PLUS a cursor pointing to where the next page should start.
On the frontend, use an IntersectionObserver watching a small 'sentinel' element near the bottom of the currently-loaded list — when it becomes visible, fetch the next page using the last cursor received. Also handle the real edge cases: prevent firing duplicate requests if the user scrolls fast (track an isLoading flag), show a distinct 'end of list reached' state once the server returns no further cursor, and handle a failed page load with a retry option rather than silently breaking scroll.
GET /api/posts?cursor=lastSeenPostId&limit=20
// frontend:
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !isLoading && hasMore) loadNextPage();
});
observer.observe(sentinelRef.current);
183How would you prevent overselling limited inventory when many users try to buy the last item at once?Advanced+
Easy explanation
This is fundamentally a CONCURRENCY problem, and the fix must live in the DATABASE, not the frontend — never trust a frontend check like 'if stock > 0, allow the purchase', because two requests can both read 'stock = 1' at the same instant, both think it's available, and both proceed to purchase the same last item.
The correct fix uses an ATOMIC, conditional database update within a single statement: `UPDATE stock SET qty = qty - 1 WHERE id = $1 AND qty > 0`, then checking how many rows were actually affected — if 0 rows were updated, someone else already took the last unit, and this purchase should be rejected. This works because the database guarantees this read-check-and-write happens as one indivisible operation, closing the race condition window that a separate 'check, then write' from the application layer would leave open.
UPDATE stock SET qty = qty - 1 WHERE product_id = $1 AND qty > 0 RETURNING qty;
// if this query affects 0 rows, someone else already got the last unit — reject this purchase
184Design a notification system (in-app + email/push).Advanced+
Easy explanation
Core flow: domain events in your app (a comment on your post, an order shipped) trigger notification CREATION — persisted as rows in a notifications table (recipient, type, read/unread, payload) so a user can see their notification history even if they weren't online when it happened. For in-app REAL-TIME delivery, push the new notification over a WebSocket/SSE connection if the user is currently connected.
For email/push notifications, DON'T send them synchronously in the same request that triggered the event — put the work on a background queue instead, so a slow third-party email API doesn't block or slow down the user action that triggered it, and so failures can be automatically retried without the original request needing to know or care. Also respect user notification preferences (some users mute certain notification types) by checking them before ever queueing the delivery.
event (e.g. 'order shipped') -> save notification row (unread) -> push over WebSocket if user online
-> queue background job -> send email/push (with retry on failure)
185How would you prevent duplicate form submissions (like a user double-clicking 'Submit')?Advanced+
Easy explanation
For USER EXPERIENCE, disable the submit button immediately after the first click (before the response returns) so a double-click can't fire two requests from the UI itself — but this is only a UX nicety, not real protection, since it can be bypassed (a slow network causing a user to click again before the button visually disables, a replayed request, or an API called directly).
The REAL fix must live server-side: an Idempotency-Key header, generated once by the client per logical action and resent on any retry, lets the server recognize 'I've already processed this exact key' and return the ORIGINAL result instead of creating a duplicate resource. For operations naturally tied to a uniqueness constraint (like one order per cart checkout), a database-level unique constraint provides an additional, foundational safety net even if the idempotency-key logic has a bug.
POST /orders
Idempotency-Key: 8d8f6b2e-4a91-...
// server: if this key was already processed, return the stored original result instead of creating a new order
186How would you design role-based access control (RBAC) for an app with admins, editors, and viewers?Advanced+
Easy explanation
Define a clear, centralized mapping of ROLES to PERMISSIONS (not scattered `if (user.role === 'admin')` checks copy-pasted across dozens of routes) — for example, 'editor' maps to `['post:create','post:edit']`, 'admin' maps to every permission. Authentication first confirms who the user is; then a single, reusable authorization check (`requirePermission(user, 'post:delete')`) is applied consistently wherever that action happens.
Critically, this check must happen on the SERVER for every protected action, not just hiding a delete button in the UI for non-admins — hiding a button is a UX nicety, not security, since a user could still call the underlying API directly. For more complex needs (permissions that depend on OWNERSHIP, like 'editors can edit their OWN posts but not others'), this evolves into attribute-based access control, checking not just the role but the specific resource's relationship to the requesting user.
const permissions = {
viewer: ['post:read'],
editor: ['post:read','post:create','post:edit'],
admin: ['post:read','post:create','post:edit','post:delete','user:manage'],
};
function requirePermission(user, perm){
if (!permissions[user.role]?.includes(perm)) throw new ForbiddenError();
}
187How would you handle a sudden, large traffic spike (like a product going viral)?Advanced+
Easy explanation
First, MEASURE where the actual bottleneck is before reacting — is it the database, the app server's CPU, a slow third-party API call, or bandwidth for static assets? Scaling the wrong layer wastes time and money while users keep suffering; a common mistake is adding more app server instances when the database was actually the saturated resource all along, which doesn't help at all.
Layered defenses, roughly from 'cheapest and fastest to add' to 'more involved': serve static assets and cacheable API responses through a CDN so they never even reach your origin servers; add caching (Redis) in front of expensive, frequently-repeated database reads; enable autoscaling so app instances scale up automatically under load; add rate limiting to protect against abusive or accidental request storms; and as a last resort, gracefully DEGRADE — temporarily disable expensive non-critical features (like personalized recommendations) to preserve capacity for the core critical path (like checkout) rather than letting everything fail together.
users -> CDN (static + cacheable) -> load balancer -> autoscaled app instances -> Redis cache -> database
// measure first: is the DB, the app CPU, or an external API call actually the bottleneck?
188How would you migrate a production database schema safely, without downtime?Advanced+
Easy explanation
Never make a single, all-at-once BREAKING schema change against a live production database serving real traffic — old application code that's still running (during a rolling deploy, some instances are on the old code while new ones deploy) would suddenly break against a changed schema it doesn't understand. The safe pattern is 'expand and contract', done in separate, sequential deploys.
EXPAND: add the new column/table alongside the old one (purely additive, doesn't break anything currently running). Deploy application code that can read/write BOTH the old and new shape. BACKFILL existing data into the new shape, often in small batches to avoid locking the whole table at once. Switch reads/writes fully over to the new shape once backfilling is verified complete. Only THEN, in a LATER, separate deploy, CONTRACT — remove the old column/table, once you're certain nothing is still depending on it.
1. expand: ALTER TABLE users ADD COLUMN full_name TEXT; -- purely additive
2. deploy code that writes to BOTH first_name/last_name AND full_name
3. backfill: UPDATE users SET full_name = first_name || ' ' || last_name WHERE full_name IS NULL;
4. switch reads to full_name once backfill is verified complete
5. contract (later deploy): ALTER TABLE users DROP COLUMN first_name, DROP COLUMN last_name;
189What should a production-readiness checklist include before shipping a new service?Advanced+
Easy explanation
At minimum: automated tests covering the critical paths (not just happy-path unit tests), server-side input validation on every endpoint, authentication AND authorization enforced correctly (not just hidden in the UI), secrets stored in a proper secret manager (never hardcoded or committed), database migrations tested and reversible, and backups actually verified to be restorable (an untested backup is not a real backup).
Beyond correctness: observability (logs, metrics, traces, and actual alerting tied to them — not just data collected that nobody looks at), a working health check endpoint, rate limiting on public endpoints, a genuine rollback plan if the deploy goes wrong, graceful shutdown handling, and clear on-call/incident ownership — knowing exactly who gets paged and what their first steps should be when something breaks at 3am.
tests -> validation -> auth/authz -> secrets management -> migrations + verified backups
-> observability + alerting -> health checks -> rate limits -> rollback plan -> on-call ownership
Tailwind, Sass & Animation
Utility-First vs Component-First CSS
Neither approach is objectively 'correct' — utility-first trades a longer class list for zero context-switching and no naming decisions; component-first trades a cleaner template for a separate file you need to maintain and name well.
190What is utility-first CSS and what's the actual trade-off vs writing custom classes?Beginner+
Easy explanation
Utility-first CSS (Tailwind's approach) composes styling by stacking many small, single-purpose classes directly in your markup — `px-4 py-2 rounded-lg font-semibold` — instead of inventing a custom class name like `.btn-primary` and writing its rules in a separate stylesheet. The benefit: you never have to invent class names, context-switch between an HTML file and a CSS file, or worry about a shared class accidentally being overridden somewhere else in a large codebase, since utilities are tiny and composable.
The trade-off: markup gets visually noisier with long class strings, and there's no single obvious place to see 'what does a primary button look like everywhere' without either extracting a component or grepping for the same class combination — which is exactly why frameworks like React pair naturally with Tailwind, since the component itself already IS the reusable unit, not a hand-named CSS class.
<button className="px-4 py-2 rounded-lg bg-blue-600 text-white font-semibold hover:bg-blue-700">
Save
</button>
191What are Tailwind's responsive variants and how do they compare to hand-written media queries?Intermediate+
Easy explanation
Tailwind's breakpoint prefixes (`sm:`, `md:`, `lg:`, `xl:`) apply a utility class only once the viewport reaches that breakpoint's minimum width — which is mobile-first by design, matching the mobile-first CSS approach covered earlier: the un-prefixed class is your default/smallest-screen style, and each prefix adds/overrides behavior at larger sizes.
Compared to hand-writing `@media` queries, this keeps the responsive behavior for one element visible right next to that element in the markup (`grid-cols-1 md:grid-cols-2 lg:grid-cols-3`), instead of having to jump between the HTML and a separate stylesheet section to see how a component's layout changes across breakpoints.
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 1 column on mobile, 2 on tablet, 3 on desktop -->
</div>
192When is Sass (or plain nested CSS) still a better fit than a utility framework?Intermediate+
Easy explanation
Sass adds features on top of plain CSS at compile time — variables, nesting, mixins (reusable blocks of styles with parameters), and functions — which historically filled gaps that vanilla CSS lacked. Modern native CSS has closed many of these gaps itself (CSS custom properties, native nesting, container queries), which is why plain CSS is a much more viable choice today than a few years ago.
Sass (or a component-scoped CSS approach) still tends to fit better in codebases with a small, highly custom design system where a component's styling is genuinely unique and complex enough that expressing it purely through utility classes becomes awkward, or in teams already deeply invested in an existing Sass architecture where rewriting everything in a utility framework isn't worth the migration cost.
// Sass mixin — reusable, parameterized style block
@mixin flex-center($gap: 0) {
display: flex; align-items: center; justify-content: center; gap: $gap;
}
.toolbar { @include flex-center(12px); }
193What is GSAP and when would you reach for a JS animation library instead of pure CSS?Intermediate+
Easy explanation
GSAP (GreenSock Animation Platform) is a JavaScript animation library built for precise, complex, and highly orchestrated animation timelines — sequencing multiple elements with exact timing offsets, scrubbing an animation based on scroll position, or physics-like easing that's awkward or impossible to express with pure CSS transitions/keyframes alone.
The general rule: reach for CSS transitions/keyframes first for simple state-driven animations (hover effects, a modal fading in) since they're lighter-weight and don't need JS at all. Reach for a JS animation library once you need programmatic control — dynamically calculated values, complex sequencing across many elements, or interactivity tied to scroll/gesture/data — that plain CSS genuinely can't express cleanly.
gsap.timeline()
.to('.card', { y: -20, duration: 0.4 })
.to('.card', { opacity: 1, duration: 0.3 }, '-=0.2'); // overlaps with the previous step
194What is Framer Motion (Motion for React) used for, and what does it add beyond CSS transitions?Intermediate+
Easy explanation
Motion for React (formerly Framer Motion) provides declarative animation primitives designed specifically for React's component model — you describe `initial`, `animate`, and `exit` states as plain objects, and the library handles interpolating between them, including animations for components being REMOVED from the DOM (via AnimatePresence), which plain CSS has no native way to handle at all since a removed element simply disappears immediately.
It also handles gesture-based interactions (drag, hover, tap animations) and 'layout animations' — automatically animating an element smoothly to its new position/size when a layout change happens (like a list reordering), which would otherwise require manually calculating FLIP-technique transforms yourself in plain CSS/JS.
import { motion, AnimatePresence } from 'framer-motion';
<AnimatePresence>
{show && (
<motion.div initial={{opacity:0,y:10}} animate={{opacity:1,y:0}} exit={{opacity:0,y:-10}}>
Content
</motion.div>
)}
</AnimatePresence>
195Why do transform and opacity animate more smoothly than properties like width, top, or margin?Advanced+
Easy explanation
Animating layout-affecting properties like width, height, top, left, or margin forces the browser to recalculate LAYOUT for potentially the entire page on every single animation frame (since changing one element's size can shift everything around it), then repaint, then composite — this is expensive work that can cause visible jank, especially on lower-powered devices.
transform (translate, scale, rotate) and opacity can often be handled entirely by the compositor — a separate, GPU-accelerated stage that doesn't need to touch layout or repaint at all — making animations built from these two properties dramatically smoother. The practical takeaway: prefer animating `transform: translateY(...)` over animating `top`, and `transform: scale(...)` over animating `width`/`height`, whenever you have the choice.
/* smoother: compositor-only, no layout recalculation */
.card:hover { transform: translateY(-4px); }
/* more expensive: triggers layout recalculation on every frame */
.card:hover { top: -4px; }
196What is prefers-reduced-motion and why should every animated site implement it?Advanced+
Easy explanation
`prefers-reduced-motion` is a media query that reflects a setting the USER has explicitly turned on at the operating-system level, indicating they experience discomfort, dizziness, or distraction from large or fast animations — this isn't a niche edge case, it's a real, documented accessibility need (related to vestibular disorders) that a meaningful number of real users rely on.
Respecting it doesn't mean removing ALL animation — it typically means removing or drastically shortening large parallax effects, auto-playing motion, or big sliding/zooming transitions, while small, functional feedback (like a button's color changing) can usually remain. Implementing this is a genuinely low-effort, high-impact accessibility win that a surprising number of otherwise polished sites skip.
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}
197How do you structure Sass with nesting and the 7-1 pattern, and what's the risk of nesting too deeply?Advanced+
Easy explanation
Sass nesting lets you write child selectors visually inside their parent's rule block, mirroring your HTML's structure and avoiding repeating the parent selector — `.card { .title { ... } }` compiles to `.card .title { ... }`. Larger Sass codebases often organize files using the '7-1 pattern' (7 folders — base, components, layout, pages, themes, abstracts, vendors — compiled through a single main entry point) to keep hundreds of partial files manageable.
The risk with nesting: going too many levels deep (`.page .sidebar .card .title span`) produces a very high-specificity, brittle selector in the compiled CSS that's hard to override later and tightly couples your styles to one exact HTML structure — a widely cited guideline is to avoid nesting more than 3 levels deep.
// risky: compiles to an overly specific, brittle selector
.page { .sidebar { .card { .title { span { color: red; } } } } }
// better: flatter, more reusable
.card-title-highlight { color: red; }
Prisma, Drizzle & ORMs
How an ORM Sits Between Your Code and the Database
An ORM lets you write queries as typed function calls instead of raw SQL strings, and translates them into real SQL under the hood — but the SQL still runs, and still has the same performance characteristics, so understanding what it generates still matters.
198What is an ORM and what problem does it actually solve?Intermediate+
Easy explanation
An ORM (Object-Relational Mapper) lets you interact with a relational database using your programming language's native objects and function calls instead of writing raw SQL strings — `db.user.findUnique({ where: { id } })` instead of hand-writing `SELECT * FROM users WHERE id = $1`. This improves productivity (autocomplete, type safety, less repetitive boilerplate) and reduces certain classes of bugs like forgetting to parameterize a query and opening yourself up to SQL injection.
It does NOT remove the need to understand the underlying database — an ORM still generates real SQL that has to run against real indexes, real query plans, and real transaction semantics. A team that treats the ORM as a total black box, with nobody understanding what SQL it's actually generating, tends to hit painful performance surprises (like accidental N+1 queries) further down the road.
// looks simple, but still generates and runs a real SQL query underneath
const user = await prisma.user.findUnique({ where: { id: 42 } });
199What is Prisma and what does its schema-first workflow look like?Intermediate+
Easy explanation
Prisma centers around a `schema.prisma` file where you declare your data models in a dedicated schema language, separate from your actual application code. From that single schema, Prisma generates a fully-typed client library specific to YOUR models (so `prisma.user.findMany()` is autocompletable and type-checked based on your exact schema), and also manages database migrations derived from changes to that schema file.
This schema-first approach gives you one authoritative source of truth for your data model that both drives your database structure AND your application's types simultaneously — if you rename a field in the schema, TypeScript will immediately flag every place in your code still using the old name, since the generated client's types update automatically.
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
// generated, fully-typed usage:
const users = await prisma.user.findMany({ include: { posts: true } });
200What is Drizzle ORM and how is its philosophy different from Prisma's?Intermediate+
Easy explanation
Drizzle is a TypeScript-first query builder/ORM that deliberately stays much closer to actual SQL than Prisma does — instead of a separate schema language and a generated client, you define your schema directly in TypeScript, and Drizzle's query API reads almost like SQL itself, just with full static typing layered on top.
The practical trade-off: Drizzle gives you more direct, transparent control over the exact SQL being generated (appealing to teams who want to reason about performance precisely and avoid 'ORM magic'), while Prisma trades a bit of that direct SQL-level transparency for a friendlier, more abstracted API and a more opinionated, batteries-included migration/tooling workflow.
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').notNull() });
const rows = await db.select().from(users).where(eq(users.email, 'a@b.com'));
201What is the ORM N+1 query problem and how do you fix it?Advanced+
Easy explanation
The N+1 problem happens when fetching a list of N parent records, and then — because of how an ORM's relation-loading convenience works — issuing ONE ADDITIONAL query PER parent record to fetch each one's related data, resulting in 1 + N total queries instead of 1 or 2 efficient ones. This most often sneaks in when a loop naively calls `await post.author` (or similar lazy relation access) for every item in a list one at a time.
The fix is to explicitly tell the ORM to fetch the related data UPFRONT, in a single batched query (or a JOIN), using whatever eager-loading feature it provides — Prisma's `include`, or a Drizzle query with an explicit join — instead of letting each parent's relation get fetched lazily and separately inside a loop.
// N+1: one extra query per post inside the loop
const posts = await prisma.post.findMany();
for (const post of posts) { const author = await prisma.user.findUnique({ where:{id:post.authorId} }); }
// fixed: one single query fetches everything upfront
const posts2 = await prisma.post.findMany({ include: { author: true } });
202What are database migrations and why are they version-controlled alongside your code?Advanced+
Easy explanation
A migration is a versioned, incremental description of a schema change — 'add a bio column to users', 'create a new orders table' — that can be applied to move a database from one known schema state to the next, and (ideally) rolled back if needed. Migrations are checked into source control right alongside your application code specifically because a given version of your CODE only works correctly against a MATCHING version of the database schema.
This lets a team, a CI pipeline, and every environment (local, staging, production) apply the exact same sequence of schema changes in the exact same order, keeping everyone's database structure in sync with the code that expects it — without migrations, 'works on my machine' schema drift between developers' local databases becomes a real, recurring problem.
npx prisma migrate dev --name add_user_bio
# generates a timestamped SQL migration file, checked into git, applied consistently everywhere
203Why do you still need to understand SQL even when using an ORM every day?Advanced+
Easy explanation
An ORM's convenience methods can't hide certain fundamental realities of the underlying database: query planning, index usage, transaction isolation levels, and locking behavior are all still happening exactly as they would with raw SQL — an ORM query that LOOKS simple can still generate an inefficient, unindexed full table scan under the hood if you don't understand what it's producing.
When something is slow or behaves unexpectedly, the debugging path almost always goes THROUGH SQL — inspecting the actual generated query, running EXPLAIN ANALYZE on it, checking whether the right index exists — regardless of which ORM you use day-to-day. Teams that only ever interact with the database through ORM abstractions, with nobody able to read or reason about the generated SQL, tend to struggle significantly once a real performance problem shows up in production.
-- regardless of which ORM generated it, this is how you actually diagnose a slow query:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'a@b.com';
Additional Interview Essentials
The Critical Rendering Path
A browser can't paint anything until it has both a DOM (from HTML) and a CSSOM (from CSS) to combine into a render tree. Render-blocking CSS or synchronous scripts in the head delay this entire chain, which is why critical CSS and script placement matter for first paint speed.
204What is the critical rendering path and how do render-blocking resources delay first paint?Advanced+
Easy explanation
The critical rendering path is the sequence of steps a browser must complete before it can show ANY pixels: parse HTML into a DOM, parse CSS into a CSSOM, combine them into a render tree, compute layout (the geometry of every element), and finally paint. Nothing is shown on screen until this whole chain completes at least once for the visible content.
CSS in the `<head>` is render-blocking by default — the browser deliberately waits for ALL of it before painting, to avoid a flash of unstyled content — and a synchronous `<script>` tag additionally blocks HTML PARSING itself while it downloads and runs. This is exactly why keeping your critical CSS small, and using `defer`/`async` on scripts (covered earlier), directly speeds up how quickly a user sees something meaningful.
<!-- blocks parsing until downloaded and executed -->
<script src="heavy.js"></script>
<!-- doesn't block parsing -->
<script src="heavy.js" defer></script>
205What is optional chaining (?.) and nullish coalescing (??), and how do they differ from ||?Intermediate+
Easy explanation
Optional chaining (`?.`) safely short-circuits to `undefined` instead of throwing an error when accessing a property or calling a method on something that might be `null`/`undefined` partway through a chain — `user?.address?.city` returns `undefined` cleanly if `user` or `address` doesn't exist, instead of crashing with 'Cannot read property of undefined'.
Nullish coalescing (`??`) provides a fallback value, but ONLY when the left side is specifically `null` or `undefined` — this is the key difference from `||`, which ALSO falls back for any other 'falsy' value like `0`, `''`, or `false`. This distinction matters a lot in practice: `count || 10` would incorrectly replace an actual, valid count of `0` with `10`, while `count ?? 10` correctly only replaces `null`/`undefined`.
const city = user?.address?.city; // undefined if user or address is missing, no crash
const count = input ?? 10; // only falls back if input is null/undefined
const count2 = input || 10; // BUG: also replaces a real 0 with 10
206What is an API gateway and when does a system actually need one?Intermediate+
Easy explanation
An API gateway is a single entry point that sits in front of one or more backend services, handling cross-cutting concerns centrally — routing requests to the correct underlying service, authentication, rate limiting, request/response transformation, and observability/logging — so individual services don't each need to reimplement all of that themselves.
It adds real value specifically once you have MULTIPLE backend services that clients need to talk to through one consistent interface (a microservices architecture), or when you need centralized policy enforcement (auth, rate limits) across many endpoints. For a single, simple monolithic API, an API gateway is usually unnecessary complexity — it's a tool that earns its place as system complexity grows, not a default you reach for on day one.
client -> API Gateway (auth, rate limit, routing) -> [ users-service | orders-service | payments-service ]
207What is a webhook and how should a receiver handle it safely?Intermediate+
Easy explanation
A webhook is an HTTP callback: instead of your app repeatedly POLLING another service to check 'did anything happen yet', that other service proactively sends an HTTP POST to a URL you provide the moment a relevant event occurs (a payment succeeded, a form was submitted) — this is far more efficient than constant polling for infrequent events.
A safe webhook receiver must verify the request is genuinely from the expected sender — typically by checking a cryptographic signature sent in a header, computed from the payload and a shared secret — since a webhook URL is otherwise just a public endpoint anyone could POST fake data to. It should also be built to handle DUPLICATE deliveries idempotently (most webhook providers retry on any failure, including ones where you actually did process it successfully but failed to respond in time), typically by tracking already-processed event IDs.
app.post('/webhooks/payment', (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).end();
}
if (alreadyProcessed(req.body.eventId)) return res.status(200).end(); // idempotent
// ...process the event...
res.status(200).end();
});
208What is connection pooling for a database and why does every production app need it?Intermediate+
Easy explanation
Opening a new database connection is a genuinely expensive operation — it involves a network handshake, authentication, and setting up session state — so creating a brand-new connection for every single incoming request would add significant latency to every request and could quickly exhaust the database's maximum allowed connections under real traffic.
A connection pool creates a bounded set of connections UPFRONT and reuses them across requests — a request 'borrows' a connection from the pool, uses it, and returns it when done, rather than opening/closing one each time. Getting the pool size right is itself a real tuning problem: too small and requests queue up waiting for a free connection under load; too large and you can overwhelm the database server itself with more concurrent connections than it can efficiently handle.
import { Pool } from 'pg';
const pool = new Pool({ max: 20 }); // reuses up to 20 real connections across all incoming requests
const result = await pool.query('SELECT * FROM users WHERE id=$1', [id]);
209What is optimistic concurrency control and how does a version column prevent lost updates?Advanced+
Easy explanation
When two users load the same record, both edit it, and both save — without any protection, the second save silently overwrites the first user's changes entirely, a classic 'lost update' problem. Optimistic concurrency control detects this WITHOUT holding a lock for the entire time a user is editing (which would be impractical for, say, a form left open for 10 minutes) — instead, it checks at SAVE time whether the record has changed since it was loaded.
The common implementation: every record has a `version` (or `updated_at`) column; your UPDATE statement includes `WHERE id = $1 AND version = $2` (the version you originally read) — if another update happened in between, that WHERE clause matches zero rows, telling your application the save failed due to a conflict, so it can reload the latest data and ask the user to resolve it, rather than silently discarding someone's work.
UPDATE documents
SET content = $1, version = version + 1
WHERE id = $2 AND version = $3; -- fails to match if someone else updated it first
-- application checks rows-affected: 0 means a conflict occurred
210What are Core Web Vitals and what does each one actually measure?Advanced+
Easy explanation
Core Web Vitals are a specific set of user-experience metrics Google uses to assess real-world page quality, focused on three dimensions: loading, interactivity, and visual stability. LCP (Largest Contentful Paint) measures how long it takes for the biggest visible content element (often a hero image or heading) to render — a proxy for 'does the page feel like it's loaded'.
INP (Interaction to Next Paint) measures the responsiveness of the page to actual user interactions throughout the whole visit (it replaced the older 'First Input Delay' metric) — a proxy for 'does the page feel laggy when I click things'. CLS (Cumulative Layout Shift), covered in the CSS section, measures how much visible content unexpectedly jumps around during loading. Together they're meant to capture 'does this page feel fast and stable', measured from real user data, not just a synthetic lab test.
// conceptually: measure with real field data (from actual visitors) AND lab tools (Lighthouse) together
// LCP target: under 2.5s | INP target: under 200ms | CLS target: under 0.1
211What is code splitting and how does React.lazy help reduce initial bundle size?Advanced+
Easy explanation
By default, a bundler like Webpack or Vite packages your entire application's JavaScript into one (or a few) files that the browser must download before your app can run — for a large app, this means users pay the download cost for code belonging to pages/features they may never even visit in that session. Code splitting breaks the bundle into smaller chunks that are loaded ONLY when actually needed.
`React.lazy()` combined with dynamic `import()` is the standard way to split a React component into its own chunk, downloaded only the first time that component is actually about to render — commonly applied at the route level (each page is its own chunk) or for large, rarely-used features (like a complex chart library or rich text editor) that most visitors never open.
const Editor = React.lazy(() => import('./Editor')); // separate chunk, downloaded only when rendered
function App(){
return <Suspense fallback={<Spinner/>}><Editor/></Suspense>;
}
212Unit vs integration vs end-to-end (E2E) tests — what does each actually verify, and what's the 'testing pyramid'?Intermediate+
Easy explanation
Unit tests verify one small, isolated piece of logic (a single function, a single component's rendering logic) in complete isolation from its real dependencies (often using mocks/stubs) — they're fast to run and pinpoint exactly where a bug is, but don't prove the pieces actually work TOGETHER. Integration tests verify multiple real units working together as they actually would in the app — like a route handler talking to a real (or realistic test) database — catching bugs that only appear at the boundaries between pieces.
End-to-end tests drive the actual running application through a real browser, simulating a genuine user flow (login, add to cart, checkout) — these give the highest confidence that a critical path genuinely works, but are the slowest to run and the most brittle (a small UI change can break many E2E tests at once). The 'testing pyramid' is the heuristic that you should have MANY fast unit tests, a moderate number of integration tests, and only a SMALL number of expensive E2E tests focused on your most critical user flows — not the other way around.
// unit: tests one function in isolation
expect(calculateTotal([{price:10,qty:2}])).toBe(20);
// E2E (e.g. Playwright/Cypress): drives a real browser through an actual user flow
await page.click('text=Add to cart'); await expect(page.locator('.cart-count')).toHaveText('1');
213Monolith vs microservices — what's the honest trade-off, and which should a small team start with?Advanced+
Easy explanation
A monolith keeps all of an application's logic in one deployable codebase — simpler to develop locally, simpler to deploy (one thing to ship), simpler to debug (one process, one set of logs), and simpler to keep data consistent (usually one shared database with real transactions). Microservices split an application into many independently deployable services, each often owning its own data — enabling independent scaling and independent deployment of each piece, and letting different teams work with more autonomy.
The honest trade-off: microservices trade that independence for genuinely significant new complexity — network calls between services (which can fail, unlike an in-process function call), distributed data consistency (no more simple ACID transactions across service boundaries), and much heavier observability/operational needs. Most experienced engineers recommend starting with a well-structured monolith (with clear internal module boundaries) and only splitting into microservices once you've identified a real, specific scaling or organizational reason to — not by default for a new project.
| Monolith | Microservices | |
|---|---|---|
| Deployment | One unit | Many independent units |
| Data consistency | Simple — one shared DB, real transactions | Hard — distributed data, no cross-service transactions |
| Best for | Most new projects, small-to-mid teams | Large orgs with clear service boundaries and real scaling needs |
// A well-structured monolith still has clear internal boundaries, without the network overhead:
src/modules/users/
src/modules/orders/
src/modules/payments/