Coding Interview Questions

Write a slugify function and a pagination helper for a product listing.

Example

The pagination helper returns everything the UI needs (offset for the query, page numbers with ellipses):

function slugify(text: string): string {
  return text.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}

function paginate(total: number, page: number, perPage: number) {
  const pages = Math.max(1, Math.ceil(total / perPage));
  const current = Math.min(Math.max(1, Math.floor(page) || 1), pages);
  const nearby = new Set([1, pages, current - 1, current, current + 1]);
  const links: (number | '…')[] = [];
  let prev = 0;
  for (const n of [...nearby].filter((n) => n >= 1 && n <= pages).sort((a, b) => a - b)) {
    if (n - prev > 1) links.push('…');
    links.push(n);
    prev = n;
  }
  return { current, pages, skip: (current - 1) * perPage, take: perPage, links };
}

console.log(slugify('  Café Coffee Day — Cold Brew (250 ml)! '));
console.log(JSON.stringify(paginate(487, 7, 24)));
console.log(paginate(487, 999, 24).links.join(' '));
console.log(paginate(30, 1, 24).links.join(' '));

Output

cafe-coffee-day-cold-brew-250-ml
{"current":7,"pages":21,"skip":144,"take":24,"links":[1,"…",6,7,8,"…",21]}
1 … 20 21
1 2

Implement an LRU cache with a TTL, similar to how revalidate works.

Example

A Map keeps insertion order, so the first key is the least recently used. Time is passed in to keep the output deterministic:

class LruTtlCache<V> {
  private map = new Map<string, { value: V; expiresAt: number }>();
  constructor(private capacity: number, private ttl: number) {}

  get(key: string, now: number): V | undefined {
    const e = this.map.get(key);
    if (!e) return undefined;
    if (now >= e.expiresAt) { this.map.delete(key); return undefined; }
    this.map.delete(key);          // move to the end (most recently used)
    this.map.set(key, e);
    return e.value;
  }

  set(key: string, value: V, now: number): void {
    this.map.delete(key);
    this.map.set(key, { value, expiresAt: now + this.ttl });
    if (this.map.size > this.capacity) {
      const oldest = this.map.keys().next().value as string;
      this.map.delete(oldest);
      console.log('  evicted ' + oldest);
    }
  }
  keys(): string[] { return [...this.map.keys()]; }
}

const c = new LruTtlCache<string>(2, 60);
c.set('/p/1', 'Redmi', 0);
c.set('/p/2', 'boAt', 5);
c.get('/p/1', 10);              // /p/1 is now most recent
c.set('/p/3', 'Clean Code', 15); // evicts /p/2
console.log(c.keys());
console.log(c.get('/p/1', 59), c.get('/p/1', 60), c.get('/p/3', 70));

Output

  evicted /p/2
[ '/p/1', '/p/3' ]
Redmi undefined Clean Code

Write a route matcher that supports static, [id] and [...slug] segments with Next.js priority.

Example

Static segments beat dynamic ones, and dynamic beats catch-all, regardless of the order files were found:

type Params = Record<string, string | string[]>;

function score(pattern: string): number[] {
  return pattern.split('/').filter(Boolean).map((s) => (s.startsWith('[...') ? 1 : s.startsWith('[') ? 2 : 3));
}

function matchOne(pattern: string, path: string): Params | null {
  const want = pattern.split('/').filter(Boolean);
  const got = path.split('/').filter(Boolean);
  const params: Params = {};
  for (let i = 0; i < want.length; i++) {
    const w = want[i];
    if (w.startsWith('[...')) {
      if (i >= got.length) return null;
      params[w.slice(4, -1)] = got.slice(i);
      return params;
    }
    if (i >= got.length) return null;
    if (w.startsWith('[')) params[w.slice(1, -1)] = decodeURIComponent(got[i]);
    else if (w !== got[i]) return null;
  }
  return want.length === got.length ? params : null;
}

function compare(a: number[], b: number[]): number {
  for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (b[i] ?? 0) - (a[i] ?? 0);
  return 0;
}

const routes = ['/docs/[...slug]', '/products/[id]', '/products/new', '/[category]/[id]'].sort((a, b) => compare(score(a), score(b)));

function resolve(path: string): string {
  for (const r of routes) {
    const p = matchOne(r, path);
    if (p) return r + ' ' + JSON.stringify(p);
  }
  return '404';
}

for (const url of ['/products/new', '/products/42', '/books/7', '/docs/a/b', '/docs']) console.log(url.padEnd(14), resolve(url));

Output

/products/new  /products/new {}
/products/42   /products/[id] {"id":"42"}
/books/7       /[category]/[id] {"category":"books","id":"7"}
/docs/a/b      /docs/[...slug] {"slug":["a","b"]}
/docs          404

Implement a fetch helper with timeout and retry with backoff (for a Server Component data call).

Example

The network is replaced by a fake fetcher that fails twice, so the program is deterministic. In real code pass signal to fetch:

async function withRetry<T>(
  fn: (signal: AbortSignal) => Promise<T>,
  { retries = 3, baseMs = 20, timeoutMs = 100 } = {},
): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn(AbortSignal.timeout(timeoutMs));
    } catch (err) {
      const msg = (err as Error).message;
      if (attempt > retries) throw new Error('Giving up after ' + attempt + ' attempts: ' + msg);
      const wait = baseMs * 2 ** (attempt - 1);
      console.log('attempt ' + attempt + ' failed (' + msg + '), retrying in ' + wait + ' ms');
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}

let calls = 0;
const fakeFetch = async (_signal: AbortSignal) => {
  calls++;
  if (calls <= 2) throw new Error('503 Service Unavailable');
  return { id: 42, name: 'Redmi Note 14', price: 17999 };
};

async function main() {
  const product = await withRetry(fakeFetch);
  console.log('got', product.name, 'after', calls, 'calls');
  try {
    await withRetry(async () => { throw new Error('ECONNREFUSED'); }, { retries: 1, baseMs: 5 });
  } catch (e) {
    console.log((e as Error).message);
  }
}
main();

Output

attempt 1 failed (503 Service Unavailable), retrying in 20 ms
attempt 2 failed (503 Service Unavailable), retrying in 40 ms
got Redmi Note 14 after 3 calls
attempt 1 failed (ECONNREFUSED), retrying in 5 ms
Giving up after 2 attempts: ECONNREFUSED

Tip: Only retry idempotent reads (GET) and transient errors (timeouts, 502/503/504). Never blindly retry a payment POST.

Compute filter facets (brand counts and price buckets) for a listing sidebar.

Example

Given products and the active filters, return counts for each brand (ignoring the brand filter itself, like Flipkart/Myntra do) and price buckets:

type Product = { name: string; brand: string; price: number };
type Filters = { brands: string[]; maxPrice?: number };

const products: Product[] = [
  { name: 'Redmi Note 14', brand: 'Redmi', price: 17999 },
  { name: 'Redmi 13C', brand: 'Redmi', price: 8999 },
  { name: 'Galaxy M35', brand: 'Samsung', price: 15999 },
  { name: 'Galaxy S24', brand: 'Samsung', price: 64999 },
  { name: 'Narzo 70', brand: 'Realme', price: 12999 },
];

const buckets = [[0, 10000], [10000, 20000], [20000, Infinity]] as const;

function facets(items: Product[], f: Filters) {
  const byPrice = items.filter((p) => f.maxPrice === undefined || p.price <= f.maxPrice);
  const brandCounts: Record<string, number> = {};
  for (const p of byPrice) brandCounts[p.brand] = (brandCounts[p.brand] ?? 0) + 1;

  const byBrand = items.filter((p) => f.brands.length === 0 || f.brands.includes(p.brand));
  const priceCounts = buckets.map(([lo, hi]) => ({
    label: hi === Infinity ? '₹' + lo + '+' : '₹' + lo + '–' + hi,
    count: byBrand.filter((p) => p.price >= lo && p.price < hi).length,
  }));

  const results = byBrand.filter((p) => byPrice.includes(p)).map((p) => p.name);
  return { brandCounts, priceCounts, results };
}

const r = facets(products, { brands: ['Samsung'], maxPrice: 20000 });
console.log('brands :', JSON.stringify(r.brandCounts));
console.log('prices :', r.priceCounts.map((b) => b.label + ' (' + b.count + ')').join(', '));
console.log('results:', r.results);

Output

brands : {"Redmi":2,"Samsung":1,"Realme":1}
prices : ₹0–10000 (0), ₹10000–20000 (1), ₹20000+ (1)
results: [ 'Galaxy M35' ]