InterviewHack.ai
Empezar gratis
Blog/Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

August 8, 2026

fullstackbackend
Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

Comprehensive full-stack developer interview guide with 46 numbered questions covering JavaScript/TypeScript, React, CSS, REST APIs, databases, Node.js, system design, security, testing, DevOps, and advanced architecture topics. Each answer includes working code examples and production-level context.

Full-Stack Developer Interview Questions: How to Answer Like a Pro (45+)

You cleared the recruiter screen. Now comes the technical interview — and for full-stack roles, that means you can get hit from any angle: frontend rendering strategies, database indexing, system design, REST vs GraphQL, authentication flows, and somewhere in the middle, someone asks you to reverse a linked list on a whiteboard.

This guide covers 45+ real full-stack interview questions with detailed answers and working code. Not surface-level definitions — actual explanations you can use in an interview without sounding like you memorized a glossary.

Read it straight through or jump to the section you need most.


What Interviewers Actually Test in Full-Stack Interviews

Before the questions: understand the framework.

Full-stack interviewers are not testing whether you memorized docs. They test:

  1. 1Mental model depth — Do you know *why* something works, not just that it works?
  2. 2Trade-off reasoning — Can you compare two approaches without defaulting to "it depends"?
  3. 3Production awareness — Do you think about security, performance, and edge cases without being prompted?
  4. 4Communication — Can you explain a complex concept to a non-expert?

Every answer below is structured to show these four things. Adapt the framing to your own experience.


Section 1: JavaScript and TypeScript Fundamentals

1. What is the event loop and how does it work?

The answer interviewers want:

JavaScript is single-threaded. The event loop is the mechanism that lets it handle asynchronous operations without blocking.

The runtime has:

  • Call stack — executes synchronous code, one frame at a time
  • Web APIs (browser) or libuv (Node) — handles async operations like timers, HTTP, I/O
  • Task queue (macrotask) — receives callbacks from Web APIs (setTimeout, setInterval)
  • Microtask queue — receives Promise callbacks and queueMicrotask calls
  • Event loop — checks if the call stack is empty, then drains the microtask queue completely, then picks one macrotask

The key detail most candidates miss: microtasks always drain before the next macrotask executes.

js
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// Output: 1, 4, 3, 2

setTimeout with 0ms delay still goes through the macrotask queue. The Promise callback is a microtask and runs first.

Why this matters in production: If you queue an unbounded chain of microtasks (recursive Promise chains), you can starve the task queue and block rendering or I/O callbacks indefinitely.


2. Explain closures. Where do you actually use them?

A closure is a function that retains access to its lexical scope even after the outer function has returned.

js
function createCounter(initialValue = 0) {
  let count = initialValue;

  return {
    increment() { count++; },
    decrement() { count--; },
    value() { return count; }
  };
}

const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.value()); // 12

count is not accessible from outside. The returned object closes over it.

Real use cases:

  • Module pattern — encapsulate private state before ES modules existed
  • Memoization — cache results in a closed-over Map
  • Partial application — fix some arguments to a function
  • Event handlers — capture loop variables correctly (though let mostly solves this now)

The classic bug:

js
// Bug: all handlers log 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

// Fix: use let (block scope creates a new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

3. What is the difference between `==` and `===`?

=== (strict equality) compares value and type. No coercion.

== (loose equality) coerces types before comparing.

js
0 == false   // true  (false coerces to 0)
0 === false  // false (different types)

'' == false  // true
'' === false // false

null == undefined  // true  (special case in spec)
null === undefined // false

NaN == NaN  // false (NaN is never equal to itself)

In an interview: Always say you use === by default. The only valid reason to use == is intentionally checking for both null and undefined at once with value == null.


4. What are TypeScript generics and when do you use them?

Generics let you write code that works with multiple types while preserving type information.

ts
// Without generics: loses type info
function first(arr: any[]): any {
  return arr[0];
}

// With generics: type is preserved
function first<T>(arr: T[]): T {
  return arr[0];
}

const n = first([1, 2, 3]);   // n: number
const s = first(['a', 'b']);  // s: string

Real-world pattern — generic API fetcher:

ts
async function fetchJSON<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

interface User {
  id: number;
  name: string;
}

const user = await fetchJSON<User>('/api/users/1');
// user.name is typed as string

With constraints:

ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const name = getProperty({ name: 'Ana', age: 25 }, 'name'); // string

5. Explain `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any`. When do you use each?

ts
const urls = ['/api/users', '/api/posts', '/api/comments'];

// Promise.all — fails fast if any rejects
// Use when all results are required and failure of one means all fail
const [users, posts, comments] = await Promise.all(urls.map(fetchJSON));

// Promise.allSettled — waits for all, never throws
// Use when you want partial results and need to handle each failure individually
const results = await Promise.allSettled(urls.map(fetchJSON));
results.forEach(result => {
  if (result.status === 'fulfilled') console.log(result.value);
  else console.error(result.reason);
});

// Promise.race — resolves/rejects with the first settled promise
// Use for timeouts
const withTimeout = (promise: Promise<unknown>, ms: number) =>
  Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ]);

// Promise.any — resolves with the first fulfilled promise
// Use for redundancy: try multiple sources, use whichever responds first
const data = await Promise.any([
  fetchJSON('/api/primary'),
  fetchJSON('/api/replica')
]);

6. What is the difference between `null`, `undefined`, and optional chaining?

  • undefined — variable declared but not assigned; missing function parameter; missing object property
  • null — explicitly set to "no value" by the developer
ts
let x;           // undefined
let y = null;    // null

typeof undefined // 'undefined'
typeof null      // 'object' (historical bug in JS spec)

Optional chaining (?.) and nullish coalescing (??):

ts
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

// Before optional chaining
const city = user && user.profile && user.profile.address && user.profile.address.city;

// After
const city = user?.profile?.address?.city;

// With fallback
const displayCity = user?.profile?.address?.city ?? 'City not provided';

// On methods
const length = user?.profile?.getDisplayName?.()?.length;

Section 2: React and Frontend Architecture

7. What is the difference between controlled and uncontrolled components in React?

Controlled: React state is the source of truth. Every keystroke updates state, which re-renders the input.

tsx
function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

Uncontrolled: The DOM is the source of truth. You read the value via a ref when needed.

tsx
function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = () => {
    console.log(inputRef.current?.value);
  };

  return (
    <>
      <input ref={inputRef} defaultValue="" />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

When to use each:

  • Controlled: validation on every keystroke, dependent fields, formatting as-you-type
  • Uncontrolled: file inputs (always), integrating with non-React libraries, large forms where every keystroke re-rendering is expensive

8. Explain `useEffect` dependency array behavior and common pitfalls.

tsx
useEffect(() => {
  // Effect code
  return () => {
    // Cleanup (optional)
  };
}, [dep1, dep2]); // Dependency array

The three forms:

  1. 1No array — runs after every render
  2. 2Empty array [] — runs once after mount (and cleanup on unmount)
  3. 3With deps — runs when any dependency changes

Common pitfall 1 — stale closure:

tsx
// Bug: count is stale inside the interval
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // always logs 0
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps locks in count = 0
}

// Fix: use functional update form
useEffect(() => {
  const id = setInterval(() => {
    setCount(prev => prev + 1); // no dependency on count
  }, 1000);
  return () => clearInterval(id);
}, []);

Common pitfall 2 — object/function in deps:

tsx
// Bug: options is a new object on every render, causing infinite loop
useEffect(() => {
  fetchData(options);
}, [options]); // new reference every render

// Fix: useMemo or move object inside effect
const stableOptions = useMemo(() => ({ page, limit }), [page, limit]);
useEffect(() => {
  fetchData(stableOptions);
}, [stableOptions]);

9. How does React reconciliation work? What is the virtual DOM?

React keeps an in-memory representation of the UI (virtual DOM). When state changes:

  1. 1React renders a new virtual DOM tree
  2. 2Compares it to the previous tree (diffing algorithm)
  3. 3Computes the minimal set of DOM mutations needed
  4. 4Applies only those mutations to the real DOM

The diffing heuristics:

  • Elements of different types produce completely different trees (no reuse)
  • Keys help React identify which list items changed, moved, or were added/removed
tsx
// Without keys: React re-renders all items when list changes
{items.map(item => <Item {...item} />)}

// With keys: React tracks each item by ID, only updates what changed
{items.map(item => <Item key={item.id} {...item} />)}

// Never use index as key when list can reorder or items can be deleted
// This causes subtle state bugs

React Fiber (React 16+): Rewrote the reconciler to make rendering interruptible. Instead of a synchronous recursive call stack, Fiber uses a linked list of units of work that can be paused, abandoned, and restarted. This enables Concurrent Mode features like Suspense and useTransition.


10. When and how do you optimize React performance?

First principle: Don't optimize prematurely. Profile first.

The tools:

  • React DevTools Profiler — shows which components re-rendered and why
  • why-did-you-render library — logs unnecessary re-renders

The techniques:

tsx
// 1. React.memo — skips re-render if props are shallowly equal
const ExpensiveList = React.memo(function ExpensiveList({ items }: Props) {
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});

// 2. useMemo — memoize expensive computations
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// 3. useCallback — stable function reference for child components
const handleDelete = useCallback((id: string) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []); // setItems is stable, so no deps needed

// 4. Virtualization — only render visible rows
// Use react-window or TanStack Virtual for lists with 1000+ items

// 5. Code splitting — lazy load routes
const Settings = React.lazy(() => import('./pages/Settings'));

The trap: useMemo and useCallback have their own cost (memory, comparison). They only pay off when the memoized value is genuinely expensive to compute or the memoized function is passed to a React.memo child.


11. What is the difference between CSR, SSR, SSG, and ISR?

| Rendering | When HTML is built | Good for |

|-----------|-------------------|----------|

| CSR (Client-Side Rendering) | In the browser on each visit | Dashboards, auth-gated apps |

| SSR (Server-Side Rendering) | On the server per request | Pages needing fresh data + SEO |

| SSG (Static Site Generation) | At build time | Blogs, marketing pages, docs |

| ISR (Incremental Static Regeneration) | At build time, revalidated in background | E-commerce, news, content that changes hourly |

tsx
// Next.js App Router examples:

// SSG (default for server components with no dynamic data)
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug); // fetched at build time
  return <article>{post.content}</article>;
}

// SSR — force dynamic
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
  const data = await fetchLiveData(); // fetched per request
  return <div>{data.value}</div>;
}

// ISR — revalidate every 60 seconds
export const revalidate = 60;
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <div>{product.price}</div>;
}

Section 3: CSS and Responsive Design

12. Explain the CSS Box Model and `box-sizing`.

Every element is a box composed of: content → padding → border → margin.

By default (box-sizing: content-box), width and height apply to the content only. Adding padding and border makes the element larger than specified.

css
/* Default behavior — confusing */
.box {
  width: 200px;
  padding: 20px;
  border: 2px solid black;
  /* Actual rendered width: 200 + 40 + 4 = 244px */
}

/* Better — width includes padding and border */
*, *::before, *::after {
  box-sizing: border-box;
}

.box {
  width: 200px;
  padding: 20px;
  border: 2px solid black;
  /* Actual rendered width: 200px */
}

This is why almost every CSS reset sets box-sizing: border-box globally.


13. When do you use Flexbox vs CSS Grid?

Flexbox: One-dimensional layout (row OR column). Use when you're distributing items along a single axis.

Grid: Two-dimensional layout (rows AND columns). Use when you're placing items on a defined grid.

css
/* Flexbox: nav bar items */
nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.nav-logo { margin-right: auto; } /* pushes other items right */

/* Grid: page layout */
.page {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 1fr;
  min-height: 100vh;
}
.header  { grid-column: 1 / -1; }
.sidebar { grid-row: 2; }
.main    { grid-row: 2; }

/* Grid: responsive card gallery */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.5rem;
}

The rule of thumb: If you're thinking about alignment of items on a line, reach for Flexbox. If you're thinking about placing items in a two-dimensional layout, reach for Grid. They also compose well — a Grid cell can contain a Flex container.


Section 4: REST APIs and HTTP

14. What are the HTTP methods and when do you use each?

| Method | Purpose | Body | Idempotent | Safe |

|--------|---------|------|-----------|------|

| GET | Retrieve | No | Yes | Yes |

| POST | Create | Yes | No | No |

| PUT | Replace | Yes | Yes | No |

| PATCH | Partial update | Yes | No* | No |

| DELETE | Remove | No | Yes | No |

Idempotent means calling it multiple times has the same effect as calling it once.

GET    /users          → list users
GET    /users/123      → get user 123
POST   /users          → create user (body has user data)
PUT    /users/123      → replace user 123 entirely
PATCH  /users/123      → update specific fields of user 123
DELETE /users/123      → delete user 123

Common mistake: Using POST for everything because "it's simpler." This breaks caching, browser history, and clients that rely on semantic HTTP.


15. What HTTP status codes should every developer know?

2xx Success
200 OK               — standard success
201 Created          — resource created (use with POST)
204 No Content       — success with no body (use with DELETE)

3xx Redirection
301 Moved Permanently   — resource URL has changed permanently
302 Found               — temporary redirect
304 Not Modified        — client's cached version is still valid

4xx Client Error
400 Bad Request         — malformed request, validation failed
401 Unauthorized        — not authenticated (no or bad token)
403 Forbidden           — authenticated but not authorized
404 Not Found           — resource doesn't exist
409 Conflict            — duplicate resource, version conflict
422 Unprocessable Entity — valid syntax but semantic validation failed
429 Too Many Requests   — rate limited

5xx Server Error
500 Internal Server Error — generic unhandled exception
502 Bad Gateway           — upstream server error
503 Service Unavailable   — server down or overloaded

Interview trap: Know the difference between 401 and 403. 401 means "I don't know who you are, authenticate first." 403 means "I know who you are, but you don't have permission."


16. How does CORS work and how do you fix CORS errors?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When a web page makes a request to a different origin (different domain, port, or protocol), the browser blocks it unless the server explicitly allows it.

How it works:

For simple requests, the browser adds an Origin header and checks the response for Access-Control-Allow-Origin.

For "preflighted" requests (custom headers, non-simple methods), the browser first sends an OPTIONS request to check permissions, then the actual request if allowed.

Client (localhost:3000)          Server (api.example.com)
       |                                  |
       |--- OPTIONS /api/data ----------->|
       |    Origin: http://localhost:3000 |
       |<-- 204 No Content ---------------|
       |    Access-Control-Allow-Origin: http://localhost:3000
       |    Access-Control-Allow-Methods: GET, POST
       |                                  |
       |--- GET /api/data --------------->|
       |<-- 200 OK -----------------------|

Server-side fix (Express):

ts
import cors from 'cors';

app.use(cors({
  origin: process.env.NODE_ENV === 'production'
    ? 'https://yourapp.com'
    : 'http://localhost:3000',
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // needed for cookies/auth headers
}));

Critical point: CORS is a browser enforcement. Postman and server-to-server requests are not subject to CORS. If you see CORS errors, fix them on the server — never disable security on the client.


17. What is the difference between REST and GraphQL? When do you choose each?

REST:

  • Multiple endpoints, each returning a fixed shape
  • Simple to cache (URL = cache key)
  • Well-understood by every tool, proxy, and CDN
  • Can over-fetch (get more fields than needed) or under-fetch (need multiple calls)

GraphQL:

  • Single endpoint, client specifies exactly what data it needs
  • Solves over-fetching and under-fetch in one query
  • Harder to cache (POST queries)
  • Excellent for complex, nested data with many client types (web, mobile, TV)
graphql
# GraphQL query — client asks for exactly what it needs
query GetUserWithPosts {
  user(id: "123") {
    name
    email
    posts(first: 5) {
      title
      publishedAt
    }
  }
}

When to use REST:

  • Simple CRUD APIs
  • Public APIs used by third parties
  • When HTTP caching is critical

When to use GraphQL:

  • Multiple clients with different data needs (mobile uses less data than desktop)
  • Rapidly evolving APIs where the frontend team moves fast
  • Complex relationships between entities

Section 5: Databases

18. What is the difference between SQL and NoSQL databases? How do you choose?

SQL (relational): Structured schema, ACID transactions, powerful joins, vertical scaling by default.

NoSQL: Flexible schema, horizontal scaling, designed for specific data patterns (documents, key-value, wide-column, graph).

Choose SQL when:

  • Data is relational (users have orders, orders have items)
  • You need ACID transactions (financial, inventory)
  • Data shape is known and stable
  • Complex queries with aggregations and joins

Choose NoSQL when:

  • Data shape varies between records (product catalog with different attributes per category)
  • You need to scale writes horizontally
  • High-throughput, low-latency key lookups (caching, sessions)
  • Storing unstructured data (logs, events, sensor data)

The honest answer in an interview: PostgreSQL can handle most workloads. Start relational, reach for NoSQL only when you have a concrete problem it solves.


19. Explain database indexing. What types of indexes exist and what are the trade-offs?

An index is a data structure (usually a B-tree) that lets the database find rows without scanning the entire table.

sql
-- Without index: full table scan, O(n)
SELECT * FROM orders WHERE user_id = 123;

-- With index: B-tree lookup, O(log n)
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Composite index — order matters
-- This index helps queries filtering by (user_id) AND (user_id + status)
-- but NOT queries filtering only by status
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Partial index — only index rows matching condition
-- Saves space when you mostly query active orders
CREATE INDEX idx_active_orders ON orders(user_id)
WHERE status = 'active';

-- Unique index — enforces constraint and enables fast lookups
CREATE UNIQUE INDEX idx_users_email ON users(email);

Trade-offs:

  • Indexes speed up reads but slow down writes (insert/update/delete must maintain the index)
  • Each index uses disk space and memory
  • Too many indexes on a write-heavy table degrades write throughput

How to identify missing indexes:

sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- Look for "Seq Scan" (bad) vs "Index Scan" (good) in the output

20. What are database transactions and ACID properties?

A transaction groups multiple operations into a single unit that either all succeed or all fail.

ACID:

  • Atomicity — all operations succeed or none do (no partial updates)
  • Consistency — the database moves from one valid state to another (constraints enforced)
  • Isolation — concurrent transactions don't interfere with each other
  • Durability — committed transactions survive crashes (written to disk)
sql
-- Classic example: bank transfer
BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 'sender';

UPDATE accounts
SET balance = balance + 100
WHERE id = 'receiver';

-- If either fails, ROLLBACK; otherwise:
COMMIT;

With Node.js + Postgres:

ts
const client = await pool.connect();
try {
  await client.query('BEGIN');
  
  await client.query(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
    [100, senderId]
  );
  
  await client.query(
    'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
    [100, receiverId]
  );
  
  await client.query('COMMIT');
} catch (err) {
  await client.query('ROLLBACK');
  throw err;
} finally {
  client.release();
}

21. What is an N+1 query problem and how do you fix it?

N+1 occurs when you fetch a list of N items, then run one additional query per item to get related data.

ts
// N+1 problem: 1 query for users + N queries for posts
const users = await db.query('SELECT * FROM users LIMIT 100'); // 1 query

for (const user of users.rows) {
  const posts = await db.query(
    'SELECT * FROM posts WHERE user_id = $1',
    [user.id]
  ); // 100 queries
  user.posts = posts.rows;
}

// Fix: JOIN or use IN
const result = await db.query(`
  SELECT
    u.id, u.name,
    p.id as post_id, p.title
  FROM users u
  LEFT JOIN posts p ON p.user_id = u.id
  ORDER BY u.id
`);

// Or with DataLoader (batches requests within a tick):
// Popular in GraphQL resolvers
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.query(
    'SELECT * FROM posts WHERE user_id = ANY($1)',
    [userIds]
  );
  return userIds.map(id => posts.rows.filter(p => p.user_id === id));
});

Section 6: Node.js and Backend Architecture

22. How does Node.js handle concurrency if it's single-threaded?

Node.js uses the event loop + non-blocking I/O (libuv thread pool) to handle many concurrent operations without blocking the main thread.

When you do fs.readFile, the work goes to libuv's thread pool (4 threads by default, configurable with UV_THREADPOOL_SIZE). When it completes, the callback is queued on the event loop.

           ┌─────────────────┐
           │   Event Loop    │ ← single thread (your JS)
           └────────┬────────┘
                    │ delegates I/O
           ┌────────▼────────┐
           │  libuv thread   │
           │     pool        │ ← disk, DNS, crypto
           └─────────────────┘
           Also: OS kernel async I/O for sockets/network

What blocks the event loop (never do this):

  • CPU-intensive synchronous code (sorting huge arrays, crypto without crypto.subtle)
  • fs.readFileSync in a hot path
  • Infinite loops

Fix for CPU-intensive work:

ts
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

if (isMainThread) {
  const worker = new Worker(__filename, {
    workerData: { numbers: [1, 2, 3, ...largeArray] }
  });
  worker.on('message', result => console.log(result));
} else {
  const result = expensiveComputation(workerData.numbers);
  parentPort?.postMessage(result);
}

23. What is middleware in Express? Write a practical example.

Middleware is a function with access to (req, res, next). It sits between the incoming request and the route handler.

ts
import express, { Request, Response, NextFunction } from 'express';

const app = express();

// 1. Request logging middleware
function requestLogger(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next(); // pass control to next middleware
}

// 2. Authentication middleware
async function requireAuth(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const payload = verifyJWT(token);
    req.user = payload; // attach to request object
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// 3. Error handling middleware (4 arguments — Express detects this)
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
}

// Apply globally
app.use(requestLogger);
app.use(express.json());

// Apply to specific routes
app.get('/profile', requireAuth, async (req, res) => {
  res.json(req.user);
});

// Error handler — must be last
app.use(errorHandler);

24. How do you design and implement JWT authentication?

JWT (JSON Web Token) is a self-contained token with three base64-encoded parts: header, payload, signature.

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ.abc123
       header              payload              signature

Full implementation:

ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m'; // access token: short-lived
const REFRESH_EXPIRES_IN = '7d';

// Login
app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await db.findUserByEmail(email);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });

  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    JWT_SECRET,
    { expiresIn: REFRESH_EXPIRES_IN }
  );

  // Store refresh token hash in DB (so we can revoke it)
  await db.saveRefreshToken(user.id, refreshToken);

  res.json({ accessToken, refreshToken });
});

// Refresh
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  try {
    const payload = jwt.verify(refreshToken, JWT_SECRET) as { userId: string };
    const stored = await db.findRefreshToken(payload.userId, refreshToken);
    if (!stored) return res.status(401).json({ error: 'Token revoked' });

    const newAccessToken = jwt.sign(
      { userId: payload.userId },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );

    res.json({ accessToken: newAccessToken });
  } catch {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// Logout — invalidate refresh token
app.post('/auth/logout', requireAuth, async (req, res) => {
  await db.deleteRefreshToken(req.user.userId);
  res.status(204).send();
});

Security notes interviewers expect you to mention:

  • Never store JWTs in localStorage (XSS risk). Prefer httpOnly cookies.
  • Always verify the signature server-side on every request.
  • Rotate refresh tokens on each use (refresh token rotation).
  • Short-lived access tokens limit the damage of a leaked token.

25. How do you handle errors properly in an async Node.js API?

ts
// Utility: wrap async route handlers to avoid try-catch everywhere
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Custom error class
class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number,
    public code?: string
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// Route uses it cleanly
app.get('/users/:id', asyncHandler(async (req: Request, res: Response) => {
  const user = await db.findUser(req.params.id);
  
  if (!user) {
    throw new AppError('User not found', 404, 'USER_NOT_FOUND');
  }
  
  res.json(user);
}));

// Central error handler distinguishes known vs unknown errors
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code
    });
  }

  // Unknown error — log and return generic message
  console.error('Unhandled error:', err);
  res.status(500).json({ error: 'Something went wrong' });
});

Section 7: System Design and Architecture

26. How would you design a URL shortener like bit.ly?

This is a classic system design question. Walk through it systematically.

Requirements clarification:

  • Shorten a URL → short code (write)
  • Redirect from short code → original URL (read)
  • Read-heavy (1000:1 read/write ratio typical)
  • Links expire after 1 year (or never, configurable)

Data model:

sql
CREATE TABLE links (
  code        VARCHAR(8) PRIMARY KEY,
  original_url TEXT NOT NULL,
  user_id     UUID REFERENCES users(id),
  created_at  TIMESTAMP DEFAULT NOW(),
  expires_at  TIMESTAMP,
  click_count BIGINT DEFAULT 0
);

Code generation strategy:

ts
// Option 1: Random base62 string (7 chars = 62^7 = 3.5 trillion combinations)
function generateCode(length = 7): string {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let code = '';
  for (let i = 0; i < length; i++) {
    code += chars[Math.floor(Math.random() * chars.length)];
  }
  return code;
}

// Option 2: Hash-based (MD5 of URL, take first 7 chars)
// Risk: collisions — need to check DB and retry

Redirect endpoint:

ts
app.get('/:code', async (req, res) => {
  const { code } = req.params;
  
  // Check cache first
  const cached = await redis.get(`link:${code}`);
  if (cached) {
    await redis.incr(`clicks:${code}`); // async click count
    return res.redirect(301, cached);
  }
  
  const link = await db.query(
    'SELECT original_url, expires_at FROM links WHERE code = $1',
    [code]
  );
  
  if (!link.rows[0]) return res.status(404).send('Not found');
  if (link.rows[0].expires_at < new Date()) return res.status(410).send('Expired');
  
  // Cache for 24 hours
  await redis.setEx(`link:${code}`, 86400, link.rows[0].original_url);
  
  res.redirect(301, link.rows[0].original_url);
});

Scaling discussion:

  • Redis cache in front of DB for read performance
  • 301 (permanent) vs 302 (temporary) redirect — 301 lets browsers cache, reducing server load, but prevents analytics
  • For analytics: use 302, log async via a queue
  • Horizontal scaling: stateless API servers behind load balancer

27. How do you implement pagination in a REST API?

There are two main approaches with different trade-offs.

Offset-based pagination:

ts
// Request: GET /posts?page=2&limit=20
app.get('/posts', async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page as string) || 1);
  const limit = Math.min(100, parseInt(req.query.limit as string) || 20);
  const offset = (page - 1) * limit;

  const [items, total] = await Promise.all([
    db.query('SELECT * FROM posts ORDER BY created_at DESC LIMIT $1 OFFSET $2', [limit, offset]),
    db.query('SELECT COUNT(*) FROM posts')
  ]);

  res.json({
    data: items.rows,
    pagination: {
      page,
      limit,
      total: parseInt(total.rows[0].count),
      totalPages: Math.ceil(parseInt(total.rows[0].count) / limit)
    }
  });
});

Cursor-based pagination (better for real-time data):

ts
// Request: GET /posts?cursor=eyJpZCI6MjB9&limit=20
app.get('/posts', async (req, res) => {
  const limit = Math.min(100, parseInt(req.query.limit as string) || 20);
  const cursor = req.query.cursor as string | undefined;

  let query = 'SELECT * FROM posts';
  const params: unknown[] = [limit + 1]; // fetch one extra to check if there's a next page

  if (cursor) {
    const { id } = JSON.parse(Buffer.from(cursor, 'base64').toString());
    query += ' WHERE id < $2 ORDER BY id DESC LIMIT $1';
    params.push(id);
  } else {
    query += ' ORDER BY id DESC LIMIT $1';
  }

  const result = await db.query(query, params);
  const items = result.rows.slice(0, limit);
  const hasMore = result.rows.length > limit;

  const nextCursor = hasMore
    ? Buffer.from(JSON.stringify({ id: items[items.length - 1].id })).toString('base64')
    : null;

  res.json({ data: items, nextCursor });
});

When to use each:

  • Offset: page number navigation ("go to page 5"), admin panels
  • Cursor: infinite scroll, feeds, any real-time data where rows can appear between pages

28. How do you implement rate limiting?

ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });

// Global rate limit
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true, // Return RateLimit-* headers
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args) => redis.sendCommand(args)
  })
});

// Stricter limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts, try again in 15 minutes' }
});

app.use(globalLimiter);
app.post('/auth/login', authLimiter, loginHandler);

Token bucket vs sliding window:

  • Fixed window (simplest): resets counter at fixed intervals — can burst at window boundary
  • Sliding window (Redis sorted set): more accurate, no burst
  • Token bucket: bucket refills at constant rate — allows controlled bursts

29. How do you approach caching in a web application?

Cache layers (outermost to innermost):

  1. 1CDN — caches static assets and cacheable responses at edge nodes globally
  2. 2Reverse proxy cache (Nginx, Varnish) — caches at the server level
  3. 3Application cache (Redis/Memcached) — caches expensive DB queries and computed results
  4. 4Database query cache — most databases have internal caching
  5. 5In-process cache — in-memory Map or LRU cache in the Node.js process
ts
// Pattern: cache-aside (most common)
async function getUser(id: string): Promise<User> {
  const cacheKey = `user:${id}`;
  
  // 1. Check cache
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);
  
  // 2. Get from DB
  const user = await db.findUser(id);
  if (!user) throw new AppError('Not found', 404);
  
  // 3. Store in cache with TTL
  await redis.setEx(cacheKey, 3600, JSON.stringify(user)); // 1 hour
  
  return user;
}

// Cache invalidation: delete on update
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.updateUser(id, data);
  await redis.del(`user:${id}`); // invalidate
  return user;
}

Cache invalidation strategies:

  • TTL (time-to-live): simple, eventual consistency
  • Write-through: update cache on every write (always fresh, write penalty)
  • Write-around: skip cache on write, populate on next read (handles infrequent reads)
  • Event-based: invalidate via message queue when data changes

Section 8: Security

30. What are the OWASP Top 10 vulnerabilities every full-stack developer should know?

The most critical ones for interviews:

1. SQL Injection — parameterized queries always:

ts
// Vulnerable
const user = await db.query(
  `SELECT * FROM users WHERE email = '${email}'`
);
// Attacker sends: ' OR '1'='1 — returns all users

// Safe
const user = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);

2. XSS (Cross-Site Scripting):

tsx
// Dangerous — executes arbitrary JS if name contains <script>alert(1)</script>
<div dangerouslySetInnerHTML={{ __html: user.name }} />

// Safe — React escapes by default
<div>{user.name}</div>

// If you must render HTML, sanitize first:
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(user.bio) }} />

3. CSRF (Cross-Site Request Forgery):

ts
// Protect with CSRF tokens or SameSite cookies
// For SPA + JWT in httpOnly cookie:
app.use(csrf({ cookie: { sameSite: 'strict' } }));

// Or use SameSite=Strict on cookies, which prevents cross-site requests
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

4. Broken Access Control:

ts
// Always verify ownership — don't trust user-provided IDs
app.delete('/posts/:id', requireAuth, async (req, res) => {
  const post = await db.findPost(req.params.id);
  
  // Check ownership — don't just delete by ID
  if (post.userId !== req.user.userId && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  await db.deletePost(req.params.id);
  res.status(204).send();
});

5. Security Misconfiguration:

  • Remove default credentials
  • Set security headers (Helmet.js for Express)
  • Never commit .env files
  • Disable stack traces in production error responses

31. How do you store passwords securely?

Never: plain text, MD5, SHA1, unsalted SHA256.

Always: use a dedicated password hashing function designed to be slow: bcrypt, Argon2, or scrypt.

ts
import bcrypt from 'bcrypt';

// Hash on registration
const SALT_ROUNDS = 12; // higher = slower = more brute-force resistant
const hash = await bcrypt.hash(plainTextPassword, SALT_ROUNDS);
await db.saveUser({ email, passwordHash: hash });

// Verify on login — bcrypt.compare handles the salt automatically
const isValid = await bcrypt.compare(inputPassword, storedHash);
// true or false

// Argon2 (more modern, recommended for new projects):
import argon2 from 'argon2';
const hash = await argon2.hash(password);
const isValid = await argon2.verify(hash, password);

Why bcrypt, not SHA-256?

  • SHA-256 is designed to be fast (millions of hashes/second on GPU)
  • bcrypt is designed to be slow and configurable — SALT_ROUNDS = 12 means 2^12 = 4096 iterations
  • Built-in salting prevents rainbow table attacks

Section 9: Testing

32. What are unit, integration, and end-to-end tests? When do you use each?

Unit tests: Test a single function or module in isolation. Mock all dependencies.

ts
// Unit test: pure function, no DB, no HTTP
import { calculateDiscount } from './pricing';

test('applies 10% discount for orders over $100', () => {
  expect(calculateDiscount(150)).toBe(15);
  expect(calculateDiscount(80)).toBe(0);
});

Integration tests: Test how components work together. May hit a real (test) database.

ts
// Integration test: API + DB layer
import request from 'supertest';
import { app } from '../app';

describe('POST /auth/login', () => {
  it('returns JWT on valid credentials', async () => {
    const res = await request(app)
      .post('/auth/login')
      .send({ email: 'test@example.com', password: 'password123' });

    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty('accessToken');
  });

  it('returns 401 on invalid password', async () => {
    const res = await request(app)
      .post('/auth/login')
      .send({ email: 'test@example.com', password: 'wrong' });

    expect(res.status).toBe(401);
  });
});

End-to-end tests: Test the full stack from the browser. Slow, brittle, but catch real integration bugs.

ts
// E2E test with Playwright
import { test, expect } from '@playwright/test';

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'user@example.com');
  await page.fill('[data-testid="password"]', 'password123');
  await page.click('[data-testid="submit"]');
  
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Welcome');
});

The Testing Trophy (Kent C. Dodds):

  • Lots of unit tests for pure logic
  • Focus most investment on integration tests (best ROI)
  • Few E2E tests for critical user flows only

33. How do you test code that depends on external services?

ts
// Pattern 1: Mock with Jest
import { sendEmail } from './emailService';
jest.mock('./emailService');

test('sends welcome email on registration', async () => {
  const mockSend = sendEmail as jest.Mock;
  mockSend.mockResolvedValue({ messageId: '123' });

  await registerUser({ email: 'new@example.com', password: 'pass' });

  expect(mockSend).toHaveBeenCalledWith({
    to: 'new@example.com',
    subject: 'Welcome!'
  });
});

// Pattern 2: Dependency injection (easier to test without mocking framework)
class UserService {
  constructor(
    private db: Database,
    private emailService: EmailService
  ) {}

  async register(email: string, password: string) {
    const user = await this.db.createUser(email, password);
    await this.emailService.sendWelcome(email);
    return user;
  }
}

// In test:
const mockEmail = { sendWelcome: jest.fn().mockResolvedValue(undefined) };
const service = new UserService(testDb, mockEmail);

Section 10: DevOps and Deployment

34. How does Docker containerization work and why does it matter?

Docker packages your app with all its dependencies into a self-contained image. The image runs identically in development, CI, and production.

dockerfile
# Multi-stage build for Node.js app
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

# Final stage: smaller image
FROM node:20-alpine AS runner

WORKDIR /app
ENV NODE_ENV=production

# Create non-root user
RUN addgroup -S app && adduser -S app -G app
USER app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3000
CMD ["node", "dist/index.js"]
yaml
# docker-compose.yml for local development
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/myapp
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

35. What is a CI/CD pipeline and what does a good one include?

CI (Continuous Integration): automatically build and test code on every push.

CD (Continuous Deployment/Delivery): automatically deploy passing builds.

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: password
          POSTGRES_DB: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s

    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npx tsc --noEmit        # type check
      - run: npm run lint            # lint
      - run: npm test                # unit + integration tests
      - run: npm run build           # ensure build doesn't break

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4
      - run: |
          docker build -t myapp:${{ github.sha }} .
          docker push registry.example.com/myapp:${{ github.sha }}
          # trigger deployment to k8s or ECS

What a good pipeline includes:

  • Type checking
  • Linting
  • Unit and integration tests
  • Build verification
  • Security scanning (Snyk, Dependabot)
  • Automated deployment to staging
  • Manual approval gate before production

Section 11: Behavioral and Architecture Questions

36. How do you approach optimizing a slow API endpoint?

Walk through this systematically:

1. Measure first:

ts
// Add timing middleware
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    const duration = Number(process.hrtime.bigint() - start) / 1_000_000;
    if (duration > 1000) {
      console.warn(`Slow request: ${req.method} ${req.path} took ${duration}ms`);
    }
  });
  next();
});

2. Profile the database queries:

sql
EXPLAIN ANALYZE SELECT * FROM orders
JOIN users ON orders.user_id = users.id
WHERE users.email = 'user@example.com'
ORDER BY orders.created_at DESC;
-- Look for Seq Scan, high cost nodes

3. Fix the most common culprits in order:

  • Missing index → add index
  • N+1 query → use JOIN or DataLoader
  • Returning too much data → add field selection, pagination
  • Repeated expensive queries → add Redis cache
  • Blocking synchronous code → make it async or offload to worker

37. How do you handle database migrations in production?

ts
// Using a migration tool like node-postgres-migrate, Flyway, or Prisma Migrate

// Migration file: 001_add_user_indexes.sql
-- Up migration
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;

-- Down migration (rollback)
DROP INDEX IF EXISTS idx_users_email;
ALTER TABLE users DROP COLUMN IF EXISTS last_login_at;

Key practices:

  • CONCURRENTLY on index creation — avoids locking the table in production
  • Never delete columns in the same deploy as removing the code that uses them (two-phase)
  • Make migrations idempotent (IF NOT EXISTS, IF EXISTS)
  • Test rollback before deploying
  • Run migrations before the new code deploys (backward-compatible migrations)

Two-phase column removal:

Phase 1 deploy: remove code references to old_column, keep column in DB
Phase 2 deploy (next day): drop old_column from DB

38. Describe a time you debugged a hard production issue.

Structure (STAR): Situation → Task → Action → Result.

Template answer pattern:

  • Describe the observable symptom (latency spike, error rate, memory leak)
  • What you did first: checked monitoring dashboards, error logs, recent deploys
  • How you narrowed it down: bisected deploys, isolated the service, reproduced locally
  • The root cause: usually a missing index, a memory leak, an infinite retry loop, a missing await
  • What you changed
  • What you added to prevent it from recurring (alerting, test, circuit breaker)

Real example to adapt:

"We had a memory leak in our Node.js API that caused restarts every 6 hours. I checked the Datadog dashboard and saw heap memory growing linearly. I added --inspect to the Node process on staging, took heap snapshots 30 minutes apart, and diffed them in Chrome DevTools. The diff showed thousands of retained EventEmitter instances all tied to a Redis subscriber we created inside a route handler on each request, but never unsubscribed. The fix was moving the subscriber to a singleton at startup. We added a memory usage alert at 80% heap to catch this class of bug earlier."


39. How do you structure a new Node.js + TypeScript project?

src/
  config/          # env validation, db connection setup
  middleware/      # auth, logging, rate limiting, error handler
  modules/
    users/
      users.router.ts
      users.service.ts
      users.repository.ts  # DB layer
      users.schema.ts      # Zod validation schemas
      users.types.ts
    posts/
      ...
  lib/             # shared utilities (logger, redis client, etc.)
  app.ts           # Express app setup (no listen())
  server.ts        # server.listen() — separates app from server for testing
tests/
  unit/
  integration/

Why this structure:

  • router → service → repository creates clear dependency direction
  • Repository layer makes swapping databases or mocking in tests trivial
  • app.ts separate from server.ts means integration tests can import the app without starting a real server

Section 12: Advanced Full-Stack Questions

40. What is WebSockets and when do you use them over polling?

HTTP is request-response: client asks, server answers, connection closes. WebSockets are full-duplex: the connection stays open and both sides can send messages at any time.

ts
// Server: Node.js with ws library
import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

// Track connected users
const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, req) => {
  const userId = extractUserIdFromRequest(req);
  clients.set(userId, ws);

  ws.on('message', (data) => {
    const { type, payload } = JSON.parse(data.toString());
    
    if (type === 'CHAT_MESSAGE') {
      // Broadcast to recipient
      const recipientWs = clients.get(payload.recipientId);
      if (recipientWs?.readyState === WebSocket.OPEN) {
        recipientWs.send(JSON.stringify({ type: 'CHAT_MESSAGE', payload }));
      }
    }
  });

  ws.on('close', () => clients.delete(userId));
});

// Client
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'AUTH', token }));
ws.onmessage = (event) => handleMessage(JSON.parse(event.data));

Use WebSockets for: real-time chat, live notifications, collaborative editing, live dashboards, gaming, stock tickers.

Use polling for: infrequent updates where real-time latency doesn't matter. Server-Sent Events (SSE) are a good middle ground for server→client-only streaming (notifications, progress bars).


41. What is server-side rendering hydration and what is "hydration mismatch"?

In SSR (Next.js, Remix), the server renders HTML and sends it to the client. The browser shows this HTML immediately (fast FCP). Then React "hydrates" it — attaches event listeners and makes it interactive.

Hydration mismatch occurs when the HTML the server rendered doesn't match what React would render on the client. React throws a warning (or in production, re-renders from scratch, causing a flash).

tsx
// Common cause: using browser-only APIs in render
function Component() {
  // Bug: window is undefined on server, causes mismatch
  const isDesktop = window.innerWidth > 768;
  return <div>{isDesktop ? 'Desktop' : 'Mobile'}</div>;
}

// Fix 1: useState with useEffect (defer to client)
function Component() {
  const [isDesktop, setIsDesktop] = useState(false); // server: false

  useEffect(() => {
    setIsDesktop(window.innerWidth > 768); // client: actual value
  }, []);

  return <div>{isDesktop ? 'Desktop' : 'Mobile'}</div>;
}

// Fix 2: suppressHydrationWarning (only when mismatch is expected and harmless)
<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>

// Fix 3: dynamic import with ssr: false (Next.js)
const MapComponent = dynamic(() => import('./Map'), { ssr: false });

42. How do you implement optimistic UI updates?

Optimistic updates show the result immediately in the UI, before the server confirms, and roll back if the server returns an error.

tsx
// With TanStack Query (React Query)
function LikeButton({ postId, initialLikes }: Props) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (postId: string) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),

    onMutate: async (postId) => {
      // Cancel any outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['post', postId] });

      // Snapshot current value
      const previous = queryClient.getQueryData(['post', postId]);

      // Optimistically update
      queryClient.setQueryData(['post', postId], (old: Post) => ({
        ...old,
        likes: old.likes + 1,
        likedByMe: true
      }));

      return { previous };
    },

    onError: (err, postId, context) => {
      // Roll back on error
      queryClient.setQueryData(['post', postId], context?.previous);
    },

    onSettled: (data, error, postId) => {
      // Always refetch after mutation
      queryClient.invalidateQueries({ queryKey: ['post', postId] });
    }
  });

  return (
    <button onClick={() => mutation.mutate(postId)}>
      Like
    </button>
  );
}

43. Explain the Twelve-Factor App methodology.

This is a methodology for building modern, scalable, maintainable web applications.

The factors most commonly asked about:

1. Codebase — one codebase, many deploys (dev/staging/prod from same repo)

2. Dependencies — explicitly declare via package.json, never rely on system-level packages

3. Config — store config in environment variables, not in code

ts
// Bad: hard-coded config
const DB_URL = 'postgresql://localhost:5432/myapp';

// Good: from environment
const DB_URL = process.env.DATABASE_URL;
if (!DB_URL) throw new Error('DATABASE_URL must be set');

4. Backing services — treat databases, queues, caches as attached resources (interchangeable via URL)

5. Build, Release, Run — strict separation between build stage and run stage

6. Processes — stateless, share-nothing processes; state lives in backing services

ts
// Bad: session in memory
const sessions = new Map(); // lost on restart, breaks multi-instance

// Good: session in Redis
app.use(session({ store: new RedisStore({ client: redis }) }));

7. Port binding — export services via port binding, not by injecting into a webserver

8. Logs — treat logs as event streams, write to stdout, let the platform aggregate


44. How do you implement feature flags?

Feature flags let you deploy code without activating features, enabling A/B testing, gradual rollouts, and instant kill switches.

ts
// Simple implementation
interface FeatureFlags {
  newDashboard: boolean;
  aiSuggestions: boolean;
  betaOnboarding: boolean;
}

// Check from database or environment
async function getFlags(userId: string): Promise<FeatureFlags> {
  // Can be stored in Redis for fast access
  const userFlags = await redis.hGetAll(`flags:${userId}`);
  const globalFlags = await redis.hGetAll('flags:global');

  return {
    newDashboard: userFlags.newDashboard === 'true' || globalFlags.newDashboard === 'true',
    aiSuggestions: userFlags.aiSuggestions === 'true',
    betaOnboarding: globalFlags.betaOnboarding === 'true',
  };
}

// React hook
function useFeatureFlag(flag: keyof FeatureFlags): boolean {
  const { data: flags } = useQuery({
    queryKey: ['flags'],
    queryFn: () => fetch('/api/flags').then(r => r.json())
  });
  return flags?.[flag] ?? false;
}

// Usage
function Dashboard() {
  const hasNewDashboard = useFeatureFlag('newDashboard');
  return hasNewDashboard ? <NewDashboard /> : <OldDashboard />;
}

In production: use a dedicated service like LaunchDarkly, Unleash, or Flipt for targeting rules, percentage rollouts, and analytics.


45. How would you handle a memory leak in a Node.js application?

Detection:

ts
// Monitor heap usage
setInterval(() => {
  const { heapUsed, heapTotal } = process.memoryUsage();
  console.log(`Heap: ${Math.round(heapUsed / 1024 / 1024)}MB / ${Math.round(heapTotal / 1024 / 1024)}MB`);
}, 30000);

// Or use --inspect and Chrome DevTools heap snapshots
// node --inspect dist/server.js
// Open chrome://inspect, take snapshots, diff them

Common causes and fixes:

ts
// 1. Event listener leak — not removing listeners
// Bug
function setupHandler(emitter: EventEmitter) {
  emitter.on('data', handler); // never cleaned up
}

// Fix
function setupHandler(emitter: EventEmitter) {
  emitter.on('data', handler);
  return () => emitter.off('data', handler); // return cleanup function
}

// 2. Global cache without eviction
// Bug
const cache = new Map(); // grows forever

// Fix: use LRU cache with max size
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, unknown>({ max: 1000 });

// 3. Unclosed database connections
// Always use pool.release() or use with a helper that ensures release
async function withDb<T>(fn: (client: PoolClient) => Promise<T>): Promise<T> {
  const client = await pool.connect();
  try {
    return await fn(client);
  } finally {
    client.release(); // always runs, even if fn throws
  }
}

46. What is the difference between horizontal and vertical scaling?

Vertical scaling (scale up): Add more CPU/RAM to existing machines. Simple, no code changes needed, but has limits and single point of failure.

Horizontal scaling (scale out): Add more instances. More complex (requires stateless design), but theoretically unlimited and fault-tolerant.

What horizontal scaling requires:

ts
// 1. Stateless application — no local state
// Bad: storing sessions in process memory
const sessions = {};

// Good: external session store
app.use(session({ store: new RedisStore() }));

// 2. Sticky-nothing WebSockets with Redis pub/sub
// When scaling to multiple Node.js processes, WebSocket messages
// must be broadcast across all instances
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';

const io = new Server(server);
io.adapter(createAdapter(pubClient, subClient));
// Now socket.io routes messages across all instances via Redis

// 3. Distributed locks for cron jobs
// Only one instance should run a given cron job
import Redlock from 'redlock';
const redlock = new Redlock([redis]);

async function runDailyCron() {
  const lock = await redlock.acquire(['locks:daily-cron'], 30000);
  try {
    await runDailyJob();
  } finally {
    await lock.release();
  }
}

How to Use This Article in Your Interview Preparation

Week 1: Read sections 1-5 (JavaScript, React, CSS, REST). Run every code example. Change things, break them, fix them.

Week 2: Sections 6-9 (Node.js, system design, security, testing). Write the patterns from memory without looking.

Week 3: Do mock interviews. Pick 5 random questions from this list. Set a 5-minute timer. Explain your answer out loud as if talking to a real interviewer.

The day before: Review section 11 (behavioral questions). Prepare 3-4 specific stories from your own experience that cover: debugging a hard problem, optimizing performance, handling a production incident, disagreeing with a technical decision.

During the interview: When you don't know something, don't guess. Say "I haven't used that specific tool, but here's how I'd approach the problem and what I'd research." That's more impressive than a wrong confident answer.


What Separates the Candidates Who Get Offers

After reviewing hundreds of technical interviews, the pattern is consistent.

Candidates who get rejected answer questions correctly but passively. They wait for the next question.

Candidates who get offers answer questions and then say one of:

  • "The trade-off here is..."
  • "In production, you'd also need to think about..."
  • "The way I've handled this in the past is..."

They demonstrate that they've shipped real code, hit real problems, and thought past the textbook answer.

Every question in this article has that layer built in. Use it.

FAQ

What topics are covered in a full-stack developer interview?+

Full-stack interviews typically cover JavaScript and TypeScript fundamentals, React and frontend architecture, CSS and responsive design, REST API design and HTTP, SQL and NoSQL databases, Node.js and backend architecture, system design, security (OWASP Top 10), testing strategies, and DevOps basics like Docker and CI/CD pipelines.

What is the most common full-stack interview question?+

The event loop question is among the most common. Interviewers use it to test whether you understand JavaScript concurrency. The key detail most candidates miss: microtasks (Promise callbacks) always drain completely before the next macrotask (setTimeout callback) executes, regardless of timeout value.

How do I explain JWT authentication in an interview?+

Describe JWT as a self-contained token with three base64-encoded parts: header, payload, and signature. Explain the flow: short-lived access tokens (15 minutes) for API requests, long-lived refresh tokens (7 days) stored in the database to enable revocation, and httpOnly cookies rather than localStorage to prevent XSS. Mention refresh token rotation for extra security.

What is the N+1 query problem?+

N+1 occurs when you fetch N items from the database and then run one additional query per item to fetch related data, resulting in N+1 total queries. The fix is to use a JOIN or an IN clause to fetch all related data in a single query. In GraphQL, use DataLoader to batch multiple individual lookups into a single database call per request tick.

How do I answer system design questions in a full-stack interview?+

Follow a systematic pattern: clarify requirements and scale assumptions first, define the data model, describe the API contract, explain the storage layer and indexing strategy, then discuss caching, scaling, and failure modes. For example, a URL shortener requires a random code generator, a redirect endpoint with Redis caching, and a discussion of 301 vs 302 redirects for analytics trade-offs.

What is the difference between SSR, SSG, and ISR in Next.js?+

SSR (Server-Side Rendering) generates HTML on the server per request — best for pages needing fresh authenticated data. SSG (Static Site Generation) builds HTML at deploy time — best for blogs and marketing pages. ISR (Incremental Static Regeneration) builds at deploy time but revalidates in the background after a configurable interval — best for product pages or content that changes hourly but not per-request.

How do you approach a slow API endpoint?+

Measure first using query timing logs and profiling tools like EXPLAIN ANALYZE in PostgreSQL. Check for the most common culprits in order: missing database indexes (Seq Scan in query plan), N+1 queries, returning too much data without pagination, repeated expensive queries that could be cached in Redis, and any synchronous blocking code in the Node.js event loop.

What security issues should every full-stack developer know?+

The most critical from OWASP Top 10: SQL injection (always use parameterized queries), XSS (React escapes by default; sanitize HTML if using dangerouslySetInnerHTML), broken access control (always verify ownership server-side, never trust client-provided IDs), insecure password storage (use bcrypt or Argon2, never SHA-256 or MD5), and security misconfiguration (set security headers with Helmet.js, never expose stack traces in production).

Artículos relacionados

How to Answer Conflict-With-a-Coworker Interview Questions

Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews.

How to Answer 'Why Do You Want to Work Here' in Interviews

Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips.

Frontend Developer Interview Questions and How to Answer Them (50+)

Complete SEO article covering 54 frontend developer interview questions with detailed answers, real code snippets across HTML, CSS, JavaScript, React, TypeScript, accessibility, security, build tools, and testing.

Backend Developer Interview Questions: Real Answers and Strategies (50+)

A comprehensive guide covering 50+ backend developer interview questions with detailed answers, real code examples in Python, JavaScript, Java, SQL, and Go, and strategic advice for each question type — from data structures and databases to system design, concurrency, security, and architecture.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesEmpresas contratandoRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY