Node.js
Express.js
Asynchronous Programming
Recursive Directory Scan
File Listing

Async and recursive directory scan, for file listing in Nodejs and Expressjs

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Recursive file listing is a common backend task in Node.js applications. You may need it to build an asset browser, expose downloadable files through Express, or scan user-uploaded content without blocking the event loop.

The key design goal is to walk the directory tree asynchronously. That keeps the server responsive while the filesystem work is in progress and avoids tying up the process with synchronous I/O.

A Basic Async Recursive Scanner

In modern Node.js, the cleanest approach uses fs.promises with async and await. The function below walks a directory tree and returns relative file paths:

javascript
1const fs = require("fs/promises");
2const path = require("path");
3
4async function scanDir(root, current = root, results = []) {
5  const entries = await fs.readdir(current, { withFileTypes: true });
6
7  for (const entry of entries) {
8    const fullPath = path.join(current, entry.name);
9
10    if (entry.isDirectory()) {
11      await scanDir(root, fullPath, results);
12    } else if (entry.isFile()) {
13      results.push(path.relative(root, fullPath));
14    }
15  }
16
17  return results;
18}
19
20scanDir("./public")
21  .then(files => console.log(files))
22  .catch(err => console.error(err));

This version is simple and predictable. It descends into every subdirectory and collects only files.

Exposing The Result Through Express

Once the scanner works, returning the list from an Express route is straightforward:

javascript
1const express = require("express");
2const app = express();
3
4app.get("/files", async (req, res, next) => {
5  try {
6    const files = await scanDir("./public");
7    res.json({ files });
8  } catch (err) {
9    next(err);
10  }
11});
12
13app.listen(3000, () => {
14  console.log("server listening on port 3000");
15});

The route handler is asynchronous, so filesystem latency does not freeze the server. The request simply waits for the promise to resolve.

Concurrency Versus Safety

The previous scanner walks one directory at a time. That is often enough and is easier to debug. If the tree is very large, you can add limited concurrency with Promise.all, but do it carefully so you do not create too many simultaneous filesystem operations.

A simple concurrent variant looks like this:

javascript
1async function scanDirConcurrent(root, current = root) {
2  const entries = await fs.readdir(current, { withFileTypes: true });
3
4  const nested = await Promise.all(
5    entries.map(async entry => {
6      const fullPath = path.join(current, entry.name);
7      if (entry.isDirectory()) {
8        return scanDirConcurrent(root, fullPath);
9      }
10      if (entry.isFile()) {
11        return [path.relative(root, fullPath)];
12      }
13      return [];
14    })
15  );
16
17  return nested.flat();
18}

This is faster on some directory trees, but it also fans out aggressively. For very large trees, consider a queue with controlled concurrency instead of unlimited parallel recursion.

Security And Path Handling

In Express applications, directory scanning should not blindly trust user input. If a route accepts a folder name from the client, resolve it against an allowed base directory and reject traversal attempts.

For example, normalize the requested path and verify that the resolved absolute path still starts with your permitted root. Without that check, a crafted path could escape the intended directory tree.

Common Pitfalls

The most common mistake is using synchronous APIs such as fs.readdirSync inside request handlers. That blocks the event loop and hurts throughput under concurrent traffic.

Another pitfall is forgetting to handle symlinks. A symlink can point outside the expected tree or even create a recursive loop. If your application must traverse symlinks, decide on explicit rules and track visited targets.

A third issue is returning absolute filesystem paths directly to clients. Those paths can leak internal server structure. Relative paths or logical file identifiers are safer for API responses.

Summary

  • Use fs.promises with async and await for recursive directory scans in Node.js.
  • The parent Express route can await the scan without blocking the event loop.
  • Sequential recursion is simpler; concurrent recursion can be faster but needs limits.
  • Validate any user-controlled path before scanning directories.
  • Avoid sync filesystem APIs in request handlers.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions