Back to Blog

How to Recursively Find Files by Extension in Node.js: Step-by-Step Guide with Code Examples

Lineserve TeamLineserve Team
December 12, 2025
4 min read

Have you ever needed to sift through a maze of folders in your Node.js project, hunting for files with a specific extension? Whether you’re building a static site generator, automating file processing, or just organizing your codebase, recursively finding files by extension is a common task. In this tutorial, we’ll dive into the core Node.js fs module to create a custom function that traverses directories and subdirectories, filtering files just like the example in the StackOverflow question. By the end, you’ll handle both synchronous and asynchronous approaches, with tips on error handling and performance. Let’s get started!

Understanding the Basics: Recursive File Searching in Node.js

Node.js’s built-in fs module provides the tools for file system operations. To find files recursively, we’ll use fs.readdir for synchronous operations or fs.readdir with callbacks for asynchronous ones. The key is to traverse directories depth-first: read the contents of a folder, check if each item is a file or subdirectory, and recurse into subdirectories.

We’ll filter files by extension using simple string methods like path.extname. This ensures we match files ending with the desired extension, such as ‘.html’.

Step 1: Setting Up the Environment

Before coding, ensure you have Node.js installed (version 14+ recommended). Create a new file, say fileFinder.js, and require the necessary modules:

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

This sets up the foundation. The path module helps with file paths and extensions.

Step 2: Building an Asynchronous Recursive File Finder

Let’s implement the findFiles function asynchronously, as it’s non-blocking and ideal for real-world apps. Here’s a complete example:

function findFiles(dir, extension, callback) {
    const results = [];
    
    fs.readdir(dir, { withFileTypes: true }, (err, files) => {
        if (err) return callback(err);
        
        let pending = files.length;
        if (!pending) return callback(null, results);
        
        files.forEach(file => {
            const filePath = path.join(dir, file.name);
            
            if (file.isDirectory()) {
                // Recurse into subdirectory
                findFiles(filePath, extension, (err, subResults) => {
                    if (err) return callback(err);
                    results.push(...subResults);
                    if (--pending === 0) callback(null, results);
                });
            } else if (path.extname(file.name).slice(1) === extension) {
                // Filter by extension (remove leading dot)
                results.push(path.relative(dir, filePath));
                if (--pending === 0) callback(null, results);
            } else {
                if (--pending === 0) callback(null, results);
            }
        });
    });
}

// Usage example
const folder = '/project1/src';
const extension = 'html';
findFiles(folder, extension, (err, results) => {
    if (err) console.error(err);
    else console.log(results);
});

This function uses withFileTypes: true (Node.js 10+) to distinguish files from directories efficiently. It recurses into subdirectories and collects matching files with paths relative to the starting folder.

Step 3: Adding a Synchronous Version for Simplicity

For smaller directories or scripts where blocking is acceptable, a synchronous version can be cleaner:

function findFilesSync(dir, extension) {
    const results = [];
    const files = fs.readdirSync(dir, { withFileTypes: true });
    
    files.forEach(file => {
        const filePath = path.join(dir, file.name);
        
        if (file.isDirectory()) {
            results.push(...findFilesSync(filePath, extension));
        } else if (path.extname(file.name).slice(1) === extension) {
            results.push(path.relative(dir, filePath));
        }
    });
    
    return results;
}

// Usage
const results = findFilesSync(folder, extension);
console.log(results);

This is straightforward but can block the event loop for large directories—use it cautiously.

Practical Examples and Use Cases

Imagine you’re building a tool to lint all HTML files in a project. You could call findFiles('/src', 'html', callback) and then process each file for validation. Or, in a build script, use it to collect all CSS files for minification. For web developers, this is handy for automated tasks like static analysis or asset bundling.

Another use case: Scanning a codebase for configuration files (e.g., ‘.json’) to validate settings across subprojects.

Tips, Best Practices, and Common Pitfalls

  • Handle Errors Gracefully: Always check for errors in callbacks or try-catch for sync versions. File system permissions can cause issues.
  • Performance Considerations: For deep trees, asynchronous is better to avoid blocking. Avoid recursion depth limits by using iterative approaches if needed.
  • Extension Matching: Use path.extname to handle cases like ‘file.min.js’. For more advanced patterns, consider regex: if (file.name.match(new RegExp(`\.${extension}$`))).
  • Case Sensitivity: Extensions are case-sensitive on most systems; convert to lowercase if needed: extension.toLowerCase().
  • Pitfall to Avoid: Don’t forget to use path.relative for relative paths as in the example. Absolute paths can complicate things.
  • Alternatives for Ease: Libraries like glob (npm install glob) simplify this: glob('**/*.html', { cwd: folder }, callback). Or fs-extra for extended fs utilities.

Best practice: Test with small directories first to avoid overwhelming logs or performance hits.

Summary and Next Steps

You’ve now mastered recursive file finding by extension in Node.js using the fs module. We covered asynchronous and synchronous methods, filtering, and error handling, directly addressing the StackOverflow query. These techniques empower you to automate file-based tasks efficiently.

Next, experiment with the code in your projects. Try integrating a library like glob for more complex patterns. If you’re dealing with large-scale apps, explore streams for processing files on-the-fly. Happy coding, and let us know if you build something cool!

Share this article

Lineserve Team

Lineserve Team

Lineserve Team is a contributor at the Hostraha blog, sharing insights on web hosting, cloud infrastructure, and web development.

Enjoy this article?

Subscribe to our newsletter for more hosting tips, tutorials, and special offers.