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 2Implement 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 404Implement 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' ]