Navigation

Node.js

How to Create Directories Recursively

Create directory structures safely with proper permissions, error handling, and atomic operations in Node.js 2025

Table Of Contents

Quick Fix: Basic Recursive Directory Creation

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

// Modern async method (recommended)
async function createDirectory(dirPath) {
  try {
    await fsPromises.mkdir(dirPath, { recursive: true });
    console.log('Directory created successfully');
  } catch (error) {
    console.error('Error creating directory:', error);
    throw error;
  }
}

// With custom permissions
async function createDirectoryWithPermissions(dirPath, mode = 0o755) {
  try {
    await fsPromises.mkdir(dirPath, { 
      recursive: true, 
      mode: mode 
    });
    console.log('Directory created with permissions');
  } catch (error) {
    console.error('Error creating directory:', error);
    throw error;
  }
}

// Synchronous method (blocking)
function createDirectorySync(dirPath, mode = 0o755) {
  try {
    fs.mkdirSync(dirPath, { 
      recursive: true, 
      mode: mode 
    });
    console.log('Directory created synchronously');
  } catch (error) {
    console.error('Error creating directory:', error);
    throw error;
  }
}

// Check if directory was created
async function createAndVerify(dirPath) {
  try {
    await fsPromises.mkdir(dirPath, { recursive: true });
    
    // Verify creation
    const stats = await fsPromises.stat(dirPath);
    if (stats.isDirectory()) {
      console.log('Directory created and verified');
      return true;
    }
    
    throw new Error('Path exists but is not a directory');
  } catch (error) {
    console.error('Error creating/verifying directory:', error);
    return false;
  }
}

// Usage examples
async function examples() {
  // Simple directory creation
  await createDirectory('./logs/app/2025');
  
  // With specific permissions
  await createDirectoryWithPermissions('./uploads/images/thumbnails', 0o755);
  
  // Synchronous creation
  createDirectorySync('./temp/cache/files');
  
  // Create and verify
  const success = await createAndVerify('./data/processed/daily');
  console.log('Creation verified:', success);
}

The Problem: Production Directory Management System

const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const os = require('os');

// Advanced directory management service
class DirectoryManager {
  constructor(options = {}) {
    this.defaultMode = options.defaultMode || 0o755;
    this.createParents = options.createParents !== false;
    this.overwriteFiles = options.overwriteFiles === true;
    this.dryRun = options.dryRun === true;
    this.enableLogging = options.enableLogging !== false;
    this.maxDepth = options.maxDepth || 50; // Prevent infinite recursion
    this.enableBackup = options.enableBackup === true;
    this.backupSuffix = options.backupSuffix || '.backup';
    this.tempPrefix = options.tempPrefix || 'dir-create-';
  }

  // Create directory with comprehensive options
  async createDirectory(dirPath, options = {}) {
    const {
      mode = this.defaultMode,
      recursive = true,
      force = false,
      backup = this.enableBackup,
      atomic = false,
      validate = true
    } = options;

    try {
      const absolutePath = path.resolve(dirPath);
      
      // Validate input
      if (validate) {
        this.validateDirectoryPath(absolutePath);
      }

      // Check depth to prevent infinite recursion
      const depth = absolutePath.split(path.sep).length;
      if (depth > this.maxDepth) {
        throw new Error(`Directory depth exceeds maximum allowed (${this.maxDepth})`);
      }

      // Log operation if enabled
      if (this.enableLogging) {
        console.log(`Creating directory: ${absolutePath}`);
      }

      // Handle dry run
      if (this.dryRun) {
        return this.simulateDirectoryCreation(absolutePath, options);
      }

      // Check if path already exists
      const existsResult = await this.checkExistence(absolutePath);
      
      if (existsResult.exists) {
        return this.handleExistingPath(absolutePath, existsResult, {
          force,
          backup,
          mode
        });
      }

      // Create directory
      let finalPath = absolutePath;
      
      if (atomic) {
        finalPath = await this.atomicDirectoryCreation(absolutePath, {
          mode,
          recursive
        });
      } else {
        await fsPromises.mkdir(absolutePath, {
          recursive,
          mode
        });
      }

      // Verify creation and set final permissions
      await this.verifyAndSetPermissions(finalPath, mode);

      return {
        success: true,
        path: finalPath,
        created: true,
        mode: mode,
        recursive: recursive,
        timestamp: new Date()
      };
    } catch (error) {
      return this.handleCreationError(error, dirPath);
    }
  }

  // Atomic directory creation using temporary directory
  async atomicDirectoryCreation(targetPath, options) {
    const { mode, recursive } = options;
    const parentDir = path.dirname(targetPath);
    const dirName = path.basename(targetPath);
    const tempName = this.tempPrefix + dirName + '-' + Date.now();
    const tempPath = path.join(parentDir, tempName);

    try {
      // Ensure parent directory exists
      if (recursive) {
        await fsPromises.mkdir(parentDir, { recursive: true, mode });
      }

      // Create with temporary name
      await fsPromises.mkdir(tempPath, { mode });
      
      // Atomic rename to final name
      await fsPromises.rename(tempPath, targetPath);
      
      return targetPath;
    } catch (error) {
      // Clean up temp directory on failure
      try {
        await fsPromises.rmdir(tempPath);
      } catch (cleanupError) {
        // Ignore cleanup errors
      }
      throw error;
    }
  }

  // Create multiple directories concurrently
  async createMultiple(dirPaths, options = {}) {
    const {
      concurrency = 10,
      continueOnError = true,
      sharedOptions = {}
    } = options;

    const results = [];
    const errors = [];

    // Process directories in batches
    for (let i = 0; i < dirPaths.length; i += concurrency) {
      const batch = dirPaths.slice(i, i + concurrency);
      
      const batchPromises = batch.map(async (dirPath, index) => {
        try {
          const result = await this.createDirectory(dirPath, sharedOptions);
          return { index: i + index, path: dirPath, ...result };
        } catch (error) {
          const errorResult = { 
            index: i + index, 
            path: dirPath, 
            success: false,
            error: error.message 
          };
          
          if (continueOnError) {
            return errorResult;
          } else {
            throw errorResult;
          }
        }
      });

      const batchResults = await Promise.allSettled(batchPromises);
      
      batchResults.forEach(promiseResult => {
        if (promiseResult.status === 'fulfilled') {
          const result = promiseResult.value;
          
          if (result.error) {
            errors.push(result);
          } else {
            results.push(result);
          }
        } else {
          errors.push({
            index: -1,
            path: 'unknown',
            success: false,
            error: promiseResult.reason
          });
        }
      });
    }

    return {
      success: errors.length === 0 || continueOnError,
      results: results.sort((a, b) => a.index - b.index),
      errors,
      total: dirPaths.length,
      successCount: results.length,
      errorCount: errors.length
    };
  }

  // Create directory tree from structure object
  async createTree(structure, basePath = '.', options = {}) {
    const results = [];
    
    for (const [name, value] of Object.entries(structure)) {
      const currentPath = path.join(basePath, name);
      
      if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
        // Create directory and recurse
        const dirResult = await this.createDirectory(currentPath, options);
        results.push(dirResult);
        
        if (dirResult.success) {
          const subResults = await this.createTree(value, currentPath, options);
          results.push(...subResults);
        }
      } else {
        // Create directory (leaf node)
        const dirResult = await this.createDirectory(currentPath, options);
        results.push(dirResult);
      }
    }
    
    return results;
  }

  // Create temporary directory
  async createTempDirectory(options = {}) {
    const {
      prefix = 'tmp-',
      suffix = '',
      baseDir = os.tmpdir(),
      mode = this.defaultMode,
      cleanup = true
    } = options;

    try {
      const tempName = prefix + Date.now() + Math.random().toString(36).substr(2, 9) + suffix;
      const tempPath = path.join(baseDir, tempName);
      
      await fsPromises.mkdir(tempPath, { mode });
      
      const result = {
        success: true,
        path: tempPath,
        cleanup: cleanup ? () => this.removeDirectory(tempPath) : null,
        created: new Date()
      };

      // Auto-cleanup on process exit if enabled
      if (cleanup) {
        process.on('exit', () => {
          try {
            fs.rmSync(tempPath, { recursive: true, force: true });
          } catch (error) {
            // Ignore cleanup errors on exit
          }
        });
      }

      return result;
    } catch (error) {
      return {
        success: false,
        error: error.message,
        code: error.code
      };
    }
  }

  // Create directory with template structure
  async createFromTemplate(templatePath, targetPath, options = {}) {
    const { variables = {}, mode = this.defaultMode } = options;

    try {
      // Read template structure
      const template = await this.readTemplate(templatePath);
      
      // Process template with variables
      const processedStructure = this.processTemplate(template, variables);
      
      // Create target directory
      await this.createDirectory(targetPath, { mode });
      
      // Create structure from template
      const results = await this.createTree(processedStructure, targetPath, { mode });
      
      return {
        success: true,
        path: targetPath,
        template: templatePath,
        created: results.filter(r => r.success).length,
        failed: results.filter(r => !r.success).length,
        results
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        template: templatePath,
        target: targetPath
      };
    }
  }

  // Synchronous directory creation with retries
  createDirectorySync(dirPath, options = {}) {
    const {
      mode = this.defaultMode,
      recursive = true,
      maxRetries = 3,
      retryDelay = 100
    } = options;

    let lastError;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const absolutePath = path.resolve(dirPath);
        
        fs.mkdirSync(absolutePath, {
          recursive,
          mode
        });

        return {
          success: true,
          path: absolutePath,
          created: true,
          attempts: attempt + 1
        };
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries && this.isRetryableError(error)) {
          // Wait before retry
          const delay = retryDelay * Math.pow(2, attempt);
          this.sleepSync(delay);
          continue;
        }
        
        break;
      }
    }

    return {
      success: false,
      error: lastError.message,
      code: lastError.code,
      attempts: maxRetries + 1
    };
  }

  // Helper methods
  async checkExistence(dirPath) {
    try {
      const stats = await fsPromises.stat(dirPath);
      return {
        exists: true,
        isDirectory: stats.isDirectory(),
        isFile: stats.isFile(),
        permissions: stats.mode,
        size: stats.size,
        modified: stats.mtime
      };
    } catch (error) {
      if (error.code === 'ENOENT') {
        return { exists: false };
      }
      throw error;
    }
  }

  async handleExistingPath(dirPath, existsResult, options) {
    const { force, backup, mode } = options;
    
    if (existsResult.isDirectory) {
      // Directory already exists - check permissions
      await this.verifyAndSetPermissions(dirPath, mode);
      
      return {
        success: true,
        path: dirPath,
        created: false,
        existed: true,
        isDirectory: true
      };
    }
    
    if (existsResult.isFile) {
      if (!force && !this.overwriteFiles) {
        throw new Error(`Path exists as file: ${dirPath}`);
      }
      
      // Backup existing file if requested
      if (backup) {
        await this.createBackup(dirPath);
      }
      
      // Remove file and create directory
      await fsPromises.unlink(dirPath);
      await fsPromises.mkdir(dirPath, { mode });
      
      return {
        success: true,
        path: dirPath,
        created: true,
        replacedFile: true,
        backup: backup
      };
    }
    
    throw new Error(`Path exists as unknown type: ${dirPath}`);
  }

  async verifyAndSetPermissions(dirPath, mode) {
    try {
      const stats = await fsPromises.stat(dirPath);
      
      if (!stats.isDirectory()) {
        throw new Error(`Path exists but is not a directory: ${dirPath}`);
      }
      
      // Set permissions if different
      if ((stats.mode & 0o777) !== mode) {
        await fsPromises.chmod(dirPath, mode);
      }
      
      return true;
    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error(`Directory was not created: ${dirPath}`);
      }
      throw error;
    }
  }

  validateDirectoryPath(dirPath) {
    if (!dirPath || typeof dirPath !== 'string') {
      throw new Error('Directory path must be a non-empty string');
    }
    
    // Prevent path traversal
    const normalizedPath = path.normalize(dirPath);
    if (normalizedPath.includes('..')) {
      throw new Error('Path traversal not allowed');
    }
    
    // Check for invalid characters (Windows)
    if (process.platform === 'win32') {
      const invalidChars = /[<>:"|?*]/;
      if (invalidChars.test(dirPath)) {
        throw new Error('Path contains invalid characters for Windows');
      }
    }
    
    return normalizedPath;
  }

  simulateDirectoryCreation(dirPath, options) {
    return {
      success: true,
      path: dirPath,
      created: true,
      dryRun: true,
      options: options,
      wouldCreate: true
    };
  }

  handleCreationError(error, dirPath) {
    let errorCode = error.code || 'UNKNOWN';
    let errorMessage = error.message;
    
    // Provide more helpful error messages
    switch (error.code) {
      case 'EACCES':
        errorMessage = `Permission denied: Cannot create directory at ${dirPath}`;
        break;
      case 'EEXIST':
        errorMessage = `Directory already exists: ${dirPath}`;
        break;
      case 'ENOTDIR':
        errorMessage = `Parent path is not a directory: ${dirPath}`;
        break;
      case 'ENOSPC':
        errorMessage = `No space left on device: ${dirPath}`;
        break;
    }
    
    return {
      success: false,
      error: errorMessage,
      code: errorCode,
      path: dirPath,
      timestamp: new Date()
    };
  }

  async createBackup(filePath) {
    const backupPath = filePath + this.backupSuffix;
    await fsPromises.copyFile(filePath, backupPath);
    return backupPath;
  }

  async removeDirectory(dirPath) {
    try {
      await fsPromises.rm(dirPath, { recursive: true, force: true });
      return { success: true, path: dirPath };
    } catch (error) {
      return { success: false, error: error.message, path: dirPath };
    }
  }

  isRetryableError(error) {
    const retryableCodes = ['EBUSY', 'EMFILE', 'ENFILE', 'EAGAIN'];
    return retryableCodes.includes(error.code);
  }

  sleepSync(ms) {
    const start = Date.now();
    while (Date.now() - start < ms) {
      // Busy wait
    }
  }

  async readTemplate(templatePath) {
    // Implementation would read a template file
    // For demo, return a simple structure
    return {
      'src': {
        'components': {},
        'utils': {},
        'styles': {}
      },
      'tests': {},
      'docs': {}
    };
  }

  processTemplate(template, variables) {
    // Simple template processing - replace variables
    const processed = JSON.parse(JSON.stringify(template));
    
    // Could implement more sophisticated template processing here
    return processed;
  }
}

// Express middleware for directory operations
function createDirectoryMiddleware(dirManager) {
  return async (req, res, next) => {
    try {
      const { path: dirPath, mode, recursive = true } = req.body;
      
      if (!dirPath) {
        return res.status(400).json({
          error: 'Directory path required',
          code: 'PATH_MISSING'
        });
      }

      const result = await dirManager.createDirectory(dirPath, {
        mode: mode ? parseInt(mode, 8) : undefined,
        recursive: recursive
      });

      res.json(result);
    } catch (error) {
      console.error('Directory middleware error:', error);
      res.status(500).json({
        error: 'Directory creation failed',
        code: 'CREATION_ERROR'
      });
    }
  };
}

// Express application
const express = require('express');
const app = express();

// Initialize directory manager
const dirManager = new DirectoryManager({
  defaultMode: 0o755,
  enableLogging: true,
  enableBackup: false
});

app.use(express.json());

// Routes
app.post('/api/create-directory', createDirectoryMiddleware(dirManager));

app.post('/api/create-multiple', async (req, res) => {
  try {
    const { paths, options } = req.body;
    
    if (!Array.isArray(paths)) {
      return res.status(400).json({
        error: 'Paths array required',
        code: 'INVALID_INPUT'
      });
    }

    const result = await dirManager.createMultiple(paths, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/create-temp', async (req, res) => {
  try {
    const result = await dirManager.createTempDirectory(req.body);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Demonstration
async function demonstrateUsage() {
  try {
    // Basic directory creation
    const basic = await dirManager.createDirectory('./test-dirs/basic');
    console.log('Basic creation:', basic.success);

    // Multiple directories
    const multiple = await dirManager.createMultiple([
      './test-dirs/multi/dir1',
      './test-dirs/multi/dir2',
      './test-dirs/multi/dir3'
    ]);
    console.log('Multiple creation:', multiple.successCount, 'of', multiple.total);

    // Directory tree
    const tree = await dirManager.createTree({
      'project': {
        'src': {
          'components': {},
          'utils': {}
        },
        'tests': {},
        'docs': {}
      }
    }, './test-dirs');
    console.log('Tree creation:', tree.filter(r => r.success).length, 'directories');

    // Temporary directory
    const temp = await dirManager.createTempDirectory({
      prefix: 'example-',
      cleanup: true
    });
    console.log('Temp directory:', temp.success ? temp.path : temp.error);
  } catch (error) {
    console.error('Demonstration error:', error);
  }
}

module.exports = {
  DirectoryManager,
  createDirectoryMiddleware,
  app
};

Recursive directory creation solves "directory structure setup", "file organization", and "deployment automation" issues. Use fs.mkdir with recursive option, handle permissions properly, implement atomic operations for safety. Support bulk creation, template-based structures, temporary directories. Handle existing paths gracefully and provide detailed error information. Alternative: shell commands, file utilities, build tools.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js