Node.js Hands-On Coding Practice

30 complete questions with solutions, explanations, and code for TCS AI Career exam preparation

Express • Async/Await • Streams • CRUD • EventEmitter • Middleware

Table of Contents

1. Basic HTTP / Express Server Tasks

Fundamentals of creating Express servers, routing, and returning JSON responses.

1 Hello API Easy

Problem

Create an Express app that:

  • Listens on port 3000
  • GET / → returns { "message": "Hello TCS" } with status 200
  • Any other route → returns { "error": "Not Found" } with status 404
ExpressRoutingStatus Codes

Solution

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.status(200).json({ message: 'Hello TCS' });
});

// Catch-all for any other route
app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
How it works: app.get('/') handles only GET requests to the root. The app.use() at the end acts as a catch-all — any request that doesn't match a previous route falls into it. Order matters: the catch-all must come after all defined routes.
2 Health Check Endpoint Easy

Problem

Implement GET /health that returns:

{ "status": "ok", "timestamp": "<ISO string>" }

Use new Date().toISOString() for the timestamp.

ExpressJSON Response

Solution

const express = require('express');
const app = express();

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString()
  });
});

app.listen(3000);
Key point: res.json() automatically sets Content-Type: application/json and serializes the object. new Date().toISOString() returns a string like "2026-07-17T10:30:00.000Z".
3 Simple Counter API (In-Memory) Easy

Problem

Build an Express app with:

  • GET /counter → returns current count (starts at 0)
  • POST /counter/increment → increments by 1, returns new count
  • POST /counter/reset → resets to 0, returns new count
ExpressIn-Memory StateHTTP Methods

Solution

const express = require('express');
const app = express();
app.use(express.json());

let count = 0;

app.get('/counter', (req, res) => {
  res.json({ count });
});

app.post('/counter/increment', (req, res) => {
  count++;
  res.json({ count });
});

app.post('/counter/reset', (req, res) => {
  count = 0;
  res.json({ count });
});

app.listen(3000);
How it works: The count variable lives in process memory. Every time the server restarts, count resets to 0. This is fine for exam purposes — real apps would use a database. Note: POST is used for increment/reset because they change server state.

2. File System & Streams

Reading files asynchronously, getting file stats, and streaming large files.

4 Async File Reader Easy

Problem

Write a Node script that reads input.txt asynchronously, logs its content, and handles errors (e.g., file not found).

fsAsyncError Handling

Solution

const fs = require('fs');

fs.readFile('input.txt', 'utf8', (err, data) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.error('Error: File "input.txt" not found.');
    } else {
      console.error('Error reading file:', err.message);
    }
    return;
  }
  console.log('File content:\n', data);
});
Key points: fs.readFile is callback-based and non-blocking. The first argument to the callback is the error object (null if success). err.code === 'ENOENT' checks specifically for "file not found". Always handle errors gracefully.
5 File Statistics Medium

Problem

Create a function getFileStats(filePath) that uses fs.promises.stat and returns:

{ sizeInBytes: Number, createdTime: Date, modifiedTime: Date }

Also create an Express route GET /file-stats?path=... that returns this JSON.

fs.promisesasync/awaitExpress

Solution

const fs = require('fs').promises;
const express = require('express');
const app = express();

async function getFileStats(filePath) {
  const stat = await fs.stat(filePath);
  return {
    sizeInBytes: stat.size,
    createdTime: stat.birthtime,
    modifiedTime: stat.mtime
  };
}

app.get('/file-stats', async (req, res) => {
  try {
    const filePath = req.query.path;
    if (!filePath) {
      return res.status(400).json({ error: 'path query param is required' });
    }
    const stats = await getFileStats(filePath);
    res.json(stats);
  } catch (err) {
    res.status(404).json({ error: 'File not found or inaccessible' });
  }
});

app.listen(3000);
Key points: fs.promises.stat returns a Stats object. stat.size is in bytes, stat.birthtime is creation time, stat.mtime is last modified time. Using async/await with try/catch is cleaner than nested callbacks.
6 Stream Large File Medium

Problem

Implement GET /stream-file that streams a large file using fs.createReadStream, sets appropriate Content-Type, and handles stream errors.

Streamsfs.createReadStreamPiping

Solution

const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();

app.get('/stream-file', (req, res) => {
  const filePath = path.join(__dirname, 'large.txt');

  const readStream = fs.createReadStream(filePath);

  // Set headers before piping
  res.setHeader('Content-Type', 'text/plain');
  res.setHeader('Content-Disposition', 'attachment; filename="large.txt"');

  readStream.pipe(res);

  readStream.on('error', (err) => {
    console.error('Stream error:', err.message);
    if (!res.headersSent) {
      res.status(404).json({ error: 'File not found' });
    } else {
      res.end();
    }
  });
});

app.listen(3000);
How it works: createReadStream reads the file in chunks (default 64KB), so memory usage stays constant regardless of file size. .pipe(res) connects the readable stream to the writable response stream. Always handle 'error' events on streams — unhandled stream errors crash the process.

3. Promises, Async/Await & Event Loop

Core asynchronous patterns in Node.js — promises, parallel/sequential execution, and event loop behavior.

7 Promise-Based Delay Easy

Problem

Write a delay(ms) function that returns a promise resolving after ms milliseconds. Then use it to log "Start", wait 1s, log "After 1s", wait 2s, log "After 3s total".

Promisesasync/awaitsetTimeout

Solution

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
  console.log('Start');
  await delay(1000);
  console.log('After 1s');
  await delay(2000);
  console.log('After 3s total');
}

main();
How it works: setTimeout doesn't return a promise natively — we wrap it in a new Promise. The await keyword pauses the async function execution until the promise resolves, but does not block the event loop. Other code can still run while we wait.
8 Parallel File Read Medium

Problem

Given ["a.txt", "b.txt", "c.txt"], read all files in parallel using Promise.all. Return an array of contents. If any file fails, reject the whole operation.

Promise.allParallelfs.promises

Solution

const fs = require('fs').promises;

async function readFilesParallel(filePaths) {
  const promises = filePaths.map(path => fs.readFile(path, 'utf8'));
  return Promise.all(promises);
}

// Usage
const files = ['a.txt', 'b.txt', 'c.txt'];
readFilesParallel(files)
  .then(contents => {
    contents.forEach((content, i) => {
      console.log(`--- ${files[i]} ---`);
      console.log(content);
    });
  })
  .catch(err => {
    console.error('One or more files failed to read:', err.message);
  });
Key points: Promise.all takes an array of promises and resolves with an array of results (in the same order). If any promise rejects, Promise.all immediately rejects with that error. This is "fail-fast" behavior. Files are read concurrently — much faster than sequential for I/O-bound tasks.
9 Sequential File Read Medium

Problem

Same as above but read files one by one in sequence using a loop and await.

async/awaitSequentialfor...of

Solution

const fs = require('fs').promises;

async function readFilesSequential(filePaths) {
  const contents = [];
  for (const filePath of filePaths) {
    const content = await fs.readFile(filePath, 'utf8');
    contents.push(content);
  }
  return contents;
}

// Usage
const files = ['a.txt', 'b.txt', 'c.txt'];
readFilesSequential(files)
  .then(contents => console.log(contents))
  .catch(err => console.error('Read failed:', err.message));
Key difference from Q8: Using for...of with await reads each file only after the previous one finishes. This is slower for independent I/O but necessary when order matters or files depend on each other. Note: for...of works with await; .forEach does not because it doesn't await the callback.

4. REST API & CRUD-Like Tasks

Building complete CRUD APIs with validation, filtering, and error handling.

10 In-Memory Users API Medium

Problem

Build an Express app with in-memory users = []. Implement GET all, GET by id, POST, PUT, DELETE with validation (name & email required for POST/PUT).

CRUDREST APIValidationStatus Codes

Solution

const express = require('express');
const app = express();
app.use(express.json());

let users = [];
let nextId = 1;

// GET /users — return all
app.get('/users', (req, res) => {
  res.json(users);
});

// GET /users/:id — return by id
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

// POST /users — create
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  const user = { id: nextId++, name, email };
  users.push(user);
  res.status(201).json(user);
});

// PUT /users/:id — update
app.put('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });

  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  user.name = name;
  user.email = email;
  res.json(user);
});

// DELETE /users/:id
app.delete('/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'User not found' });
  users.splice(index, 1);
  res.json({ message: 'Deleted' });
});

app.listen(3000);
Key patterns: parseInt(req.params.id) converts URL params (strings) to numbers. find() returns the first match; findIndex() returns its index (or -1). splice(index, 1) removes one element at that index. Status codes: 200 (OK), 201 (Created), 400 (Bad Request), 404 (Not Found).
11 Query Params & Filtering Medium

Problem

Extend the users API so GET /users?role=admin returns only matching users. Support multiple filters like ?role=admin&active=true.

Query ParamsFilteringBoolean Parsing

Solution

// Add this to the GET /users route from Q10
app.get('/users', (req, res) => {
  let filtered = [...users]; // clone array

  // Apply each query param as a filter
  Object.keys(req.query).forEach(key => {
    let value = req.query[key];

    // Parse boolean strings
    if (value === 'true') value = true;
    else if (value === 'false') value = false;

    filtered = filtered.filter(user => user[key] === value);
  });

  res.json(filtered);
});
How it works: req.query is an object of all query parameters (always strings from URL). We iterate over each filter key and narrow down results. Boolean strings ("true"/"false") are parsed to actual booleans. For number parsing, use Number(value). This pattern is reusable for any set of filters.
12 Error-Handling Middleware Medium

Problem

Add a global error handler and a route that intentionally throws an error to test it.

Error HandlingMiddlewarenext(err)

Solution

const express = require('express');
const app = express();
app.use(express.json());

// A route that throws an error
app.get('/crash', (req, res, next) => {
  const err = new Error('Something broke on purpose!');
  err.status = 500;
  next(err); // pass error to error-handling middleware
});

// Global error handler — MUST have 4 parameters
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(err.status || 500).json({
    error: 'Something went wrong',
    message: err.message
  });
});

app.listen(3000);
Critical detail: Express error-handling middleware must have exactly 4 parameters (err, req, res, next). If you omit err, Express treats it as regular middleware. Calling next(err) with an argument skips all normal middleware and goes straight to the error handler.

5. Middleware & Auth-Like Logic

Writing custom middleware for logging, authentication, and rate limiting.

13 Logging Middleware Easy

Problem

Write middleware that logs HTTP method, URL, and timestamp for every request, then calls next().

Middlewareapp.usenext()

Solution

function requestLogger(req, res, next) {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.originalUrl}`);
  next();
}

// Apply globally
app.use(requestLogger);

// Or apply to specific routes
app.get('/users', requestLogger, (req, res) => {
  res.json(users);
});
Key points: Middleware is a function with signature (req, res, next). Always call next() to pass control to the next middleware — otherwise the request hangs forever. req.originalUrl includes query strings; req.url doesn't if mounted on a sub-router.
14 Simple Auth Middleware Medium

Problem

Check for header Authorization: Bearer <token>. If token is "secret123", call next(). Otherwise respond 401. Apply only to GET /protected.

AuthHeadersRoute-Specific Middleware

Solution

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Extract token from "Bearer <token>"
  const token = authHeader.split(' ')[1];

  if (token === 'secret123') {
    next();
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

// Apply only to GET /protected
app.get('/protected', authMiddleware, (req, res) => {
  res.json({ message: 'You have access to protected data!' });
});

// This route does NOT require auth
app.get('/public', (req, res) => {
  res.json({ message: 'Anyone can see this' });
});
Key points: Passing middleware as the second argument to a route applies it only to that route. The split(' ')[1] extracts the token after "Bearer ". In production, use JWT verification and environment variables for secrets — never hardcode them.
15 Rate Limiter (Basic) Hard

Problem

Track requests per IP. Allow max 5 requests per minute per IP. Return 429 if exceeded. Reset counts periodically.

Rate LimitingIP TrackingsetTimeout

Solution

const rateLimits = {}; // { ip: { count: N, resetTime: timestamp } }

function rateLimiter(req, res, next) {
  const ip = req.ip;
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 minute
  const maxRequests = 5;

  if (!rateLimits[ip] || now > rateLimits[ip].resetTime) {
    // First request or window expired — start new window
    rateLimits[ip] = { count: 1, resetTime: now + windowMs };
    return next();
  }

  rateLimits[ip].count++;

  if (rateLimits[ip].count > maxRequests) {
    return res.status(429).json({ error: 'Too many requests' });
  }

  next();
}

app.use(rateLimiter);

app.get('/', (req, res) => {
  res.json({ message: 'Hello' });
});
How it works: Each IP gets a sliding window of 1 minute. First request creates the window. Subsequent requests increment the counter. When the window expires (current time > resetTime), a fresh window starts. The 429 status code is the standard for rate limiting. In production, use libraries like express-rate-limit with Redis backing.

6. Event-Driven & EventEmitter Tasks

Extending EventEmitter, emitting custom events, and async event processing.

16 Custom EventEmitter Medium

Problem

Create OrderService extending EventEmitter. Method createOrder(data) emits "orderCreated". A listener logs the order id.

EventEmitterClass ExtensionEvents

Solution

const { EventEmitter } = require('events');

class OrderService extends EventEmitter {
  createOrder(data) {
    // Do order creation logic here...
    console.log('Creating order:', data);
    this.emit('orderCreated', data);
  }
}

// Usage
const orderService = new OrderService();

orderService.on('orderCreated', (data) => {
  console.log(`Order created with id: ${data.id}`);
});

orderService.createOrder({ id: 123, item: 'Laptop' });
Key points: EventEmitter is a core Node.js class. .on(event, listener) registers a listener. .emit(event, data) triggers all listeners for that event. You can have multiple listeners for the same event — they run in registration order.
17 Async Event Processing Hard

Problem

Make the event listener async (simulate DB save with await delay(500)). Ensure errors inside the listener don't crash the process.

Async EventsError Handlingtry/catch

Solution

const { EventEmitter } = require('events');

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

class OrderService extends EventEmitter {
  createOrder(data) {
    this.emit('orderCreated', data);
  }
}

const orderService = new OrderService();

// Async listener with error handling
orderService.on('orderCreated', async (data) => {
  try {
    console.log(`Processing order ${data.id}...`);
    await delay(500); // simulate DB save
    console.log(`Order ${data.id} saved to database.`);
  } catch (err) {
    console.error(`Error processing order ${data.id}:`, err.message);
    // Don't re-throw — prevents process crash
  }
});

// Also handle uncaught errors on the emitter
orderService.on('error', (err) => {
  console.error('OrderService error:', err.message);
});

orderService.createOrder({ id: 456, item: 'Phone' });

// Intentionally trigger error to show error handler
// Uncomment to test:
// orderService.on('orderCreated', async () => { throw new Error('DB down!'); });
// orderService.createOrder({ id: 789, item: 'Tablet' });
Key points: When you use async listeners with EventEmitter, the emitter doesn't await them — errors become unhandled promise rejections. Always wrap async listener bodies in try/catch. The 'error' event on EventEmitter is special: if no listener is registered, it throws and crashes the process.

7. Utility & Algorithmic Tasks

Pure JavaScript utility functions commonly asked in coding rounds.

18 Debounce Function Medium

Problem

Implement a debounce(fn, delay) that delays calling fn until after delay ms of inactivity.

ClosuresetTimeoutclearTimeout

Solution

function debounce(fn, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// Usage
const log = debounce((msg) => {
  console.log(msg, new Date().toISOString());
}, 1000);

log('Hello');  // Reset timer
log('World');  // Reset timer
log('!');      // Only this one fires after 1s of silence
How it works: Each call clears the previous timer and starts a new one. Only the last call in a rapid sequence actually executes fn. ...args captures all arguments. fn.apply(this, args) preserves the correct this context and passes all arguments.
19 Flatten Array (1 Level) Easy

Problem

Flatten a nested array by one level: [1, [2, 3], [4, [5, 6]]][1, 2, 3, 4, [5, 6]]

Arrayflat()concat

Solution

// Method 1: Built-in flat(1)
function flattenOneLevel(arr) {
  return arr.flat(1);
}

// Method 2: Using concat + spread (for interviews)
function flattenOneLevel(arr) {
  return [].concat(...arr);
}

// Method 3: Manual reduce
function flattenOneLevel(arr) {
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? val : [val]);
  }, []);
}

console.log(flattenOneLevel([1, [2, 3], [4, [5, 6]]]));
// Output: [1, 2, 3, 4, [5, 6]]
Key points: arr.flat(1) is the cleanest approach (ES2019). [].concat(...arr) spreads the array — each element is concatenated, sub-arrays stay intact (one level). The reduce approach is explicit and shows you understand the pattern. Interviewers often ask for manual implementations.
20 Group By Key Medium

Problem

Group an array of objects by a key (e.g., age):

// Input
[{ name: "A", age: 20 }, { name: "B", age: 20 }, { name: "C", age: 21 }]
// Output
{ 20: [{name:"A",age:20}, {name:"B",age:20}], 21: [{name:"C",age:21}] }
reduceObject Grouping

Solution

function groupByKey(arr, key) {
  return arr.reduce((groups, item) => {
    const val = item[key];
    if (!groups[val]) {
      groups[val] = [];
    }
    groups[val].push(item);
    return groups;
  }, {});
}

const people = [
  { name: "A", age: 20 },
  { name: "B", age: 20 },
  { name: "C", age: 21 }
];

console.log(groupByKey(people, 'age'));
How it works: reduce builds up a single object. For each item, we get the grouping value (item[key]), create an array if it doesn't exist yet, then push the item. The accumulator starts as {}. This is a classic pattern for data transformation.
21 Unique By Key Medium

Problem

Remove duplicates from an array of objects based on a key (e.g., "id"), keeping the first occurrence.

MapDeduplication

Solution

function uniqueByKey(arr, key) {
  const seen = new Map();
  return arr.filter(item => {
    if (seen.has(item[key])) return false;
    seen.set(item[key], true);
    return true;
  });
}

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice Duplicate' },
  { id: 3, name: 'Charlie' },
  { id: 2, name: 'Bob Duplicate' }
];

console.log(uniqueByKey(users, 'id'));
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
How it works: We use a Map to track seen keys. filter keeps only the first occurrence of each key. Map.has() is O(1), so this runs in O(n). Alternative: use [...new Map(arr.map(item => [item[key], item])).values()] (more concise but less readable).
22 Promise Wrapper for fs.readFile Medium

Problem

Without using fs.promises, write a function that wraps callback-based fs.readFile in a promise.

PromisesCallback Wrappingfs

Solution

const fs = require('fs');

function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

// Usage
readFileAsync('input.txt')
  .then(content => console.log(content))
  .catch(err => console.error('Error:', err.message));

// With async/await
async function main() {
  try {
    const content = await readFileAsync('input.txt');
    console.log(content);
  } catch (err) {
    console.error('Error:', err.message);
  }
}
Pattern: This is the "promisify" pattern. Inside the Promise constructor, call the callback-based function. In the callback: call resolve(data) on success, reject(err) on failure. Node.js has util.promisify that does this automatically: const readFile = util.promisify(fs.readFile).

8. Mini-Project Style

Larger, more complete exercises combining multiple concepts.

23 Todo API (In-Memory) Hard

Problem

Full CRUD: GET list, POST create, PATCH toggle/update, DELETE. Add validation (title required) and proper status codes.

CRUDPATCHValidationExpress

Solution

const express = require('express');
const app = express();
app.use(express.json());

let todos = [];
let nextId = 1;

// GET /todos — list all
app.get('/todos', (req, res) => {
  res.json(todos);
});

// POST /todos — create
app.post('/todos', (req, res) => {
  const { title, completed } = req.body;
  if (!title || typeof title !== 'string') {
    return res.status(400).json({ error: 'Title is required and must be a string' });
  }
  const todo = {
    id: nextId++,
    title,
    completed: completed || false
  };
  todos.push(todo);
  res.status(201).json(todo);
});

// PATCH /todos/:id — toggle or update
app.patch('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Todo not found' });

  const { title, completed } = req.body;
  if (title !== undefined) todo.title = title;
  if (completed !== undefined) todo.completed = completed;
  else todo.completed = !todo.completed; // toggle if not provided

  res.json(todo);
});

// DELETE /todos/:id
app.delete('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = todos.findIndex(t => t.id === id);
  if (index === -1) return res.status(404).json({ error: 'Todo not found' });
  todos.splice(index, 1);
  res.json({ message: 'Todo deleted' });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000);
PATCH vs PUT: PATCH partially updates — only provided fields change. PUT replaces the entire resource. The "toggle" behavior: if completed is in the body, use that value; otherwise flip it. This is a common pattern in todo apps.
24 File Upload Simulator Medium

Problem

POST /upload accepts JSON { filename, content }, writes to uploads/, returns success message. Handle errors.

fs.writeFilePath SafetyError Handling

Solution

const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json());

const UPLOADS_DIR = path.join(__dirname, 'uploads');

// Ensure uploads directory exists
if (!fs.existsSync(UPLOADS_DIR)) {
  fs.mkdirSync(UPLOADS_DIR);
}

app.post('/upload', (req, res) => {
  const { filename, content } = req.body;

  if (!filename || content === undefined) {
    return res.status(400).json({ error: 'filename and content are required' });
  }

  // Basic sanitization — prevent path traversal
  const safeName = path.basename(filename);
  const filePath = path.join(UPLOADS_DIR, safeName);

  fs.writeFile(filePath, content, 'utf8', (err) => {
    if (err) {
      return res.status(500).json({ error: 'Failed to write file', message: err.message });
    }
    res.json({ message: 'File saved', path: `uploads/${safeName}` });
  });
});

app.listen(3000);
Security note: path.basename(filename) strips directory parts (e.g., "../../etc/passwd" becomes "passwd"), preventing path traversal attacks. Always validate and sanitize user-supplied filenames. In production, use a library like multer for multipart form uploads.

9. Additional Practice Questions

Extra questions based on common TCS and Node.js interview patterns.

25 Promise.allSettled Results Medium

Problem

Given an array of promises (some may reject), use Promise.allSettled to get the result of each — whether fulfilled or rejected — and log a summary.

Promise.allSettledError Handling

Solution

function delay(ms, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) reject(new Error(`Failed after ${ms}ms`));
      else resolve(`Resolved after ${ms}ms`);
    }, ms);
  });
}

async function main() {
  const promises = [
    delay(100),
    delay(200, true),
    delay(300),
    delay(150, true),
  ];

  const results = await Promise.allSettled(promises);

  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`Promise ${i}: ✅ ${result.value}`);
    } else {
      console.log(`Promise ${i}: ❌ ${result.reason.message}`);
    }
  });
}

main();
Promise.allSettled vs Promise.all: allSettled never rejects — it waits for all promises and returns an array of { status, value } or { status, reason } objects. Promise.all is fail-fast. Use allSettled when you want to know which promises succeeded and which failed.
26 Simple CSV Parser Medium

Problem

Write a function that parses a CSV string into an array of objects using the first row as headers.

// Input: "name,age,city\nAlice,30,NYC\nBob,25,LA"
// Output: [{name:"Alice",age:"30",city:"NYC"}, {name:"Bob",age:"25",city:"LA"}]
String Parsingsplitmap

Solution

function parseCSV(csvString) {
  const lines = csvString.trim().split('\n');
  const headers = lines[0].split(',');

  return lines.slice(1).map(line => {
    const values = line.split(',');
    const obj = {};
    headers.forEach((header, i) => {
      obj[header] = values[i];
    });
    return obj;
  });
}

// Usage
const csv = "name,age,city\nAlice,30,NYC\nBob,25,LA";
console.log(parseCSV(csv));
// [{ name: 'Alice', age: '30', city: 'NYC' },
//  { name: 'Bob', age: '25', city: 'LA' }]
Approach: Split by newlines to get rows, first row becomes headers. For each data row, split by comma and pair each value with its header. This is a simplified parser — real CSVs handle quoted fields, escaped commas, etc. For production, use the csv-parse package.
27 Simple Task Scheduler Hard

Problem

Build a function scheduleTasks(tasks, delay) that runs an array of async functions sequentially, waiting delay ms between each.

async/awaitSchedulingfor...of

Solution

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function scheduleTasks(tasks, delayMs) {
  const results = [];
  for (const task of tasks) {
    const result = await task();
    results.push(result);
    if (delayMs > 0) {
      await delay(delayMs);
    }
  }
  return results;
}

// Usage
const tasks = [
  async () => { console.log('Task 1 done'); return 'result1'; },
  async () => { console.log('Task 2 done'); return 'result2'; },
  async () => { console.log('Task 3 done'); return 'result3'; },
];

scheduleTasks(tasks, 1000).then(results => {
  console.log('All results:', results);
});
How it works: The for...of loop with await ensures tasks run sequentially. After each task completes, we optionally wait delayMs. Results are collected in order. This pattern is useful for rate-limiting API calls or processing items with delays between them.
28 JSON to Query String Easy

Problem

Convert a JavaScript object into a URL query string: { name: "John", age: 30 }"name=John&age=30"

URLObject.entriesjoin

Solution

function toQueryString(obj) {
  return Object.entries(obj)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
    .join('&');
}

console.log(toQueryString({ name: "John", age: 30, city: "New York" }));
// "name=John&age=30&city=New%20York"

// Alternative using URLSearchParams
function toQueryString2(obj) {
  return new URLSearchParams(obj).toString();
}

console.log(toQueryString2({ name: "John", age: 30 }));
// "name=John&age=30"
Key points: Object.entries() returns [[key, value], ...]. encodeURIComponent handles special characters (spaces → %20, etc.). URLSearchParams is the built-in browser/Node API that does this automatically. Know both approaches for interviews.
29 Reverse Words in String Easy

Problem

Reverse the order of words in a string: "Hello World from Node""Node from World Hello"

Stringsplitreverse

Solution

function reverseWords(str) {
  return str.split(' ').reverse().join(' ');
}

console.log(reverseWords("Hello World from Node"));
// "Node from World Hello"

// Edge case: multiple spaces
function reverseWordsClean(str) {
  return str.trim().split(/\s+/).reverse().join(' ');
}

console.log(reverseWordsClean("  Hello   World  "));
// "World Hello"
Key points: split(' ') splits by single space. split(/\s+/) handles multiple spaces. trim() removes leading/trailing whitespace. reverse() reverses the array in place. join(' ') joins with a single space. This 3-method chain is a classic one-liner.
30 Event Loop Output Prediction Hard

Problem

Predict the output order of this code and explain why:

console.log('1');

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

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

process.nextTick(() => {
  console.log('4');
});

console.log('5');
Event LoopMicrotasksMacrotasks

Solution

// Output order:
// 1
// 5
// 4
// 3
// 2
Explanation — Event Loop Phases:
1 & 5: Synchronous code runs first (call stack).
4 (process.nextTick): Next-tick queue has highest priority among microtasks. Runs before Promises.
3 (Promise.then): Promise callbacks are microtasks — run after nextTick but before macrotasks.
2 (setTimeout): Timers/macrotasks run after all microtasks are drained.

Priority order: Synchronous → process.nextTick → Promise.then → setTimeout/setInterval

Practice Summary

Cover all topics systematically. Focus on patterns you find difficult.

30
Questions
9
Sections
10
Easy
14
Medium
6
Hard