Skip to main content

Command Palette

Search for a command to run...

Complete Guide to File Handling in Node.js and Express

Published
12 min read
Complete Guide to File Handling in Node.js and Express

File handling is a fundamental skill every Node.js developer needs to master. Whether you're building a simple web application or a complex server-side system, understanding how to read, write, and manage files efficiently is crucial. In this comprehensive guide, we'll explore everything from basic file operations using Node.js's built-in fs module to handling file uploads in Express applications with Multer.

Table of Contents

  1. Introduction to File Handling in Node.js

  2. The fs Module: Your File System Gateway

  3. Synchronous vs Asynchronous File Operations

  4. Working with Streams for Large Files

  5. File Uploads in Express with Multer

  6. Storing and Serving Uploaded Files

  7. Understanding Callbacks in File Operations

  8. Best Practices and Error Handling

  9. Complete Working Example

  10. Conclusion

Introduction to File Handling in Node.js {#introduction}

Node.js provides powerful built-in modules for file system operations, making it easy to work with files and directories. File handling in Node.js typically involves:

  • Reading files from the file system

  • Writing data to files

  • Managing file uploads from client applications

  • Serving static files to users

  • Processing large files efficiently using streams

The primary module we'll work with is the fs (File System) module, which comes built into Node.js and provides both synchronous and asynchronous methods for file operations.

The fs Module: Your File System Gateway {#fs-module}

The fs module is Node.js's built-in file system module that provides an API for interacting with the file system. Let's start with basic read and write operations.

Reading Files

There are several ways to read files in Node.js. Here are the most common approaches:

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

// Method 1: Asynchronous file reading with callback
fs.readFile("example.txt", "utf8", (err, data) => {
  if (err) {
    console.error("Error reading file:", err);
    return;
  }
  console.log("File content:", data);
});

// Method 2: Promise-based approach (Node.js 10+)
const fsPromises = require("fs").promises;

async function readFileAsync() {
  try {
    const data = await fsPromises.readFile("example.txt", "utf8");
    console.log("File content:", data);
  } catch (err) {
    console.error("Error reading file:", err);
  }
}

readFileAsync();

Writing Files

Similarly, writing files can be done in multiple ways:

const fs = require("fs");

// Writing data to a file asynchronously
const content = "Hello, this is sample content!";

fs.writeFile("output.txt", content, "utf8", (err) => {
  if (err) {
    console.error("Error writing file:", err);
    return;
  }
  console.log("File written successfully!");
});

// Promise-based writing
async function writeFileAsync() {
  try {
    await fsPromises.writeFile("output.txt", content, "utf8");
    console.log("File written successfully!");
  } catch (err) {
    console.error("Error writing file:", err);
  }
}

Synchronous vs Asynchronous File Operations {#sync-vs-async}

Understanding the difference between synchronous and asynchronous operations is crucial for Node.js development.

File Operations Flow Diagram

Application Request
       ↓
   fs Module
       ↓
┌─────────────────────────────────────┐
│              Disk I/O               │
├─────────────────┬───────────────────┤
│  Synchronous    │    Asynchronous   │
│  (Blocking)     │   (Non-blocking)  │
│       ↓         │         ↓         │
│  Waits for      │  Continues with   │
│  completion     │  other operations │
│       ↓         │         ↓         │
│  Returns data   │  Callback/Promise │
│  directly       │  when complete    │
└─────────────────┴───────────────────┘

Synchronous Operations (Blocking)

const fs = require("fs");

try {
  // This will BLOCK the entire application until the file is read
  const data = fs.readFileSync("large-file.txt", "utf8");
  console.log("File read complete");

  // This line won't execute until the file reading is done
  console.log("This runs after file reading");
} catch (err) {
  console.error("Error:", err);
}

Pros: Simple to use, easier to understand Cons: Blocks the entire application, poor performance for I/O operations

Asynchronous Operations (Non-blocking)

const fs = require("fs");

console.log("Starting file read...");

fs.readFile("large-file.txt", "utf8", (err, data) => {
  if (err) {
    console.error("Error:", err);
    return;
  }
  console.log("File read complete");
});

// This runs immediately, doesn't wait for file reading
console.log("This runs while file is being read");

Pros: Non-blocking, better performance, scalable Cons: More complex with callbacks, potential callback hell

Working with Streams for Large Files {#streams}

Streams are perfect for handling large files efficiently without loading everything into memory at once.

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

// Reading large files with streams
function readLargeFile(filePath) {
  const readStream = fs.createReadStream(filePath, { encoding: "utf8" });

  readStream.on("data", (chunk) => {
    console.log(`Received ${chunk.length} characters of data`);
    // Process chunk here
  });

  readStream.on("end", () => {
    console.log("Finished reading file");
  });

  readStream.on("error", (err) => {
    console.error("Stream error:", err);
  });
}

// Writing to files with streams
function writeLargeFile(filePath, data) {
  const writeStream = fs.createWriteStream(filePath);

  writeStream.write(data);
  writeStream.end();

  writeStream.on("finish", () => {
    console.log("Write completed");
  });

  writeStream.on("error", (err) => {
    console.error("Write error:", err);
  });
}

Piping Streams (Copy Files Efficiently)

const fs = require("fs");

function copyFile(source, destination) {
  const readStream = fs.createReadStream(source);
  const writeStream = fs.createWriteStream(destination);

  // Pipe automatically handles the data transfer
  readStream.pipe(writeStream);

  readStream.on("error", (err) => console.error("Read error:", err));
  writeStream.on("error", (err) => console.error("Write error:", err));
  writeStream.on("finish", () => console.log("Copy completed"));
}

copyFile("large-source.txt", "destination-copy.txt");

File Uploads in Express with Multer {#file-uploads}

Multer is the most popular middleware for handling multipart/form-data in Express applications, primarily used for file uploads.

Installing Dependencies

npm install express multer

Basic Multer Setup

const express = require("express");
const multer = require("multer");
const path = require("path");

const app = express();

// Basic multer configuration
const upload = multer({ dest: "uploads/" });

// Handle single file upload
app.post("/upload", upload.single("file"), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: "No file uploaded" });
  }

  console.log("Uploaded file:", req.file);
  res.json({
    message: "File uploaded successfully",
    filename: req.file.filename,
    originalname: req.file.originalname,
    size: req.file.size,
  });
});

Advanced Multer Configuration

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "uploads/");
  },
  filename: function (req, file, cb) {
    // Generate unique filename
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(
      null,
      file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
    );
  },
});

// File filter for validation
const fileFilter = (req, file, cb) => {
  // Accept only image files
  if (file.mimetype.startsWith("image/")) {
    cb(null, true);
  } else {
    cb(new Error("Only image files are allowed!"), false);
  }
};

const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB limit
  },
});

Storing and Serving Uploaded Files {#storing-serving}

Once files are uploaded, you need to store them properly and make them accessible to users.

File Upload and Storage Flow

Client Browser
       ↓ (POST with multipart/form-data)
Express Server
       ↓
Multer Middleware
       ↓ (processes file)
Disk Storage
       ↓ (saves file)
Database Record (optional)
       ↓
Response to Client
       ↓
Static File Serving (express.static)

Complete File Upload Handler

const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");

const app = express();

// Ensure uploads directory exists
const uploadsDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadsDir)) {
  fs.mkdirSync(uploadsDir, { recursive: true });
}

// Configure multer
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(
      null,
      file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
    );
  },
});

const upload = multer({
  storage: storage,
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
  fileFilter: (req, file, cb) => {
    // Allow images and documents
    const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
    const extname = allowedTypes.test(
      path.extname(file.originalname).toLowerCase()
    );
    const mimetype = allowedTypes.test(file.mimetype);

    if (mimetype && extname) {
      return cb(null, true);
    } else {
      cb(new Error("Invalid file type"));
    }
  },
});

// Serve uploaded files statically
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

// File upload endpoint
app.post("/upload", upload.single("file"), (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: "No file uploaded" });
    }

    // File successfully uploaded
    const fileInfo = {
      originalName: req.file.originalname,
      filename: req.file.filename,
      path: req.file.path,
      size: req.file.size,
      mimetype: req.file.mimetype,
      uploadDate: new Date(),
      url: `/uploads/${req.file.filename}`,
    };

    res.json({
      message: "File uploaded successfully",
      file: fileInfo,
    });
  } catch (error) {
    res.status(500).json({ error: "Upload failed", details: error.message });
  }
});

// Multiple file uploads
app.post("/upload-multiple", upload.array("files", 5), (req, res) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).json({ error: "No files uploaded" });
  }

  const fileInfos = req.files.map((file) => ({
    originalName: file.originalname,
    filename: file.filename,
    size: file.size,
    url: `/uploads/${file.filename}`,
  }));

  res.json({
    message: `${req.files.length} files uploaded successfully`,
    files: fileInfos,
  });
});

Understanding Callbacks in File Operations {#callbacks}

Callbacks are functions passed as arguments to other functions and are executed when an asynchronous operation completes.

Callback Pattern

const fs = require("fs");

// Standard Node.js callback pattern: (error, result)
function readFileWithCallback(filename, callback) {
  fs.readFile(filename, "utf8", (err, data) => {
    if (err) {
      // Error occurred - call callback with error
      callback(err, null);
      return;
    }
    // Success - call callback with data
    callback(null, data);
  });
}

// Usage
readFileWithCallback("example.txt", (err, content) => {
  if (err) {
    console.error("Error reading file:", err.message);
    return;
  }
  console.log("File content:", content);
});

Avoiding Callback Hell

const fs = require("fs");
const util = require("util");

// Convert callback-based functions to promises
const readFileAsync = util.promisify(fs.readFile);
const writeFileAsync = util.promisify(fs.writeFile);

// Instead of nested callbacks (callback hell):
/*
fs.readFile('input.txt', 'utf8', (err, data) => {
  if (err) throw err;
  const processedData = data.toUpperCase();
  fs.writeFile('output.txt', processedData, (err) => {
    if (err) throw err;
    fs.readFile('output.txt', 'utf8', (err, newData) => {
      if (err) throw err;
      console.log('Final data:', newData);
    });
  });
});
*/

// Use async/await for cleaner code:
async function processFile() {
  try {
    const data = await readFileAsync("input.txt", "utf8");
    const processedData = data.toUpperCase();
    await writeFileAsync("output.txt", processedData);
    const finalData = await readFileAsync("output.txt", "utf8");
    console.log("Final data:", finalData);
  } catch (err) {
    console.error("Error:", err);
  }
}

Best Practices and Error Handling {#best-practices}

Error Handling

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

async function safeFileOperation(filePath) {
  try {
    // Check if file exists
    await fs.promises.access(filePath, fs.constants.F_OK);

    // Read file
    const data = await fs.promises.readFile(filePath, "utf8");

    return data;
  } catch (err) {
    if (err.code === "ENOENT") {
      throw new Error(`File not found: ${filePath}`);
    } else if (err.code === "EACCES") {
      throw new Error(`Permission denied: ${filePath}`);
    } else {
      throw new Error(`File operation failed: ${err.message}`);
    }
  }
}

File Validation

function validateUploadedFile(file) {
  const errors = [];

  // Check file size (5MB limit)
  if (file.size > 5 * 1024 * 1024) {
    errors.push("File size exceeds 5MB limit");
  }

  // Check file type
  const allowedTypes = [
    "image/jpeg",
    "image/png",
    "image/gif",
    "application/pdf",
  ];
  if (!allowedTypes.includes(file.mimetype)) {
    errors.push("Invalid file type");
  }

  // Check filename
  const filename = file.originalname;
  if (!filename || filename.length > 255) {
    errors.push("Invalid filename");
  }

  return {
    isValid: errors.length === 0,
    errors: errors,
  };
}

Complete Working Example {#complete-example}

Here's a complete Express application that demonstrates all the concepts covered:

const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");

const app = express();
const port = 3000;

// Ensure directories exist
const uploadsDir = path.join(__dirname, "uploads");
const publicDir = path.join(__dirname, "public");

[uploadsDir, publicDir].forEach((dir) => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// Multer configuration
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, "uploads/"),
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(
      null,
      file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
    );
  },
});

const upload = multer({
  storage: storage,
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
  fileFilter: (req, file, cb) => {
    const allowedTypes = /jpeg|jpg|png|gif|pdf/;
    const extname = allowedTypes.test(
      path.extname(file.originalname).toLowerCase()
    );
    const mimetype = allowedTypes.test(file.mimetype);

    if (mimetype && extname) {
      cb(null, true);
    } else {
      cb(new Error("Only images and PDFs are allowed"));
    }
  },
});

// Middleware
app.use(express.json());
app.use(express.static("public"));
app.use("/uploads", express.static("uploads"));

// Routes
app.get("/", (req, res) => {
  res.send(`
    <html>
      <body>
        <h1>File Upload Demo</h1>
        <form action="/upload" method="post" enctype="multipart/form-data">
          <input type="file" name="file" required>
          <button type="submit">Upload</button>
        </form>
        <br>
        <h3>Multiple Files:</h3>
        <form action="/upload-multiple" method="post" enctype="multipart/form-data">
          <input type="file" name="files" multiple required>
          <button type="submit">Upload Multiple</button>
        </form>
      </body>
    </html>
  `);
});

// Single file upload
app.post("/upload", upload.single("file"), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: "No file uploaded" });
  }

  res.json({
    message: "File uploaded successfully",
    file: {
      originalName: req.file.originalname,
      filename: req.file.filename,
      size: req.file.size,
      url: `/uploads/${req.file.filename}`,
    },
  });
});

// Multiple files upload
app.post("/upload-multiple", upload.array("files", 5), (req, res) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).json({ error: "No files uploaded" });
  }

  const files = req.files.map((file) => ({
    originalName: file.originalname,
    filename: file.filename,
    size: file.size,
    url: `/uploads/${file.filename}`,
  }));

  res.json({
    message: `${req.files.length} files uploaded successfully`,
    files: files,
  });
});

// List uploaded files
app.get("/files", async (req, res) => {
  try {
    const files = await fs.promises.readdir(uploadsDir);
    const fileList = [];

    for (const file of files) {
      const filePath = path.join(uploadsDir, file);
      const stats = await fs.promises.stat(filePath);

      fileList.push({
        name: file,
        size: stats.size,
        created: stats.birthtime,
        url: `/uploads/${file}`,
      });
    }

    res.json({ files: fileList });
  } catch (err) {
    res.status(500).json({ error: "Unable to list files" });
  }
});

// Delete file
app.delete("/files/:filename", async (req, res) => {
  try {
    const filename = req.params.filename;
    const filePath = path.join(uploadsDir, filename);

    await fs.promises.unlink(filePath);
    res.json({ message: "File deleted successfully" });
  } catch (err) {
    if (err.code === "ENOENT") {
      res.status(404).json({ error: "File not found" });
    } else {
      res.status(500).json({ error: "Unable to delete file" });
    }
  }
});

// Error handling middleware
app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === "LIMIT_FILE_SIZE") {
      return res.status(400).json({ error: "File too large" });
    }
    if (error.code === "LIMIT_FILE_COUNT") {
      return res.status(400).json({ error: "Too many files" });
    }
  }

  res.status(500).json({ error: error.message });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
  console.log("Upload files at http://localhost:${port}");
});

Testing the Application

  1. Save the code as app.js

  2. Run npm init -y && npm install express multer

  3. Run node app.js

  4. Visit http://localhost:3000 to test file uploads

Conclusion {#conclusion}

File handling in Node.js and Express is a powerful feature that enables you to build robust applications. Here's a summary of key takeaways:

Key Concepts Covered:

  1. fs Module Fundamentals: Understanding both synchronous and asynchronous file operations, with a strong preference for asynchronous methods in production applications.

  2. Streams for Efficiency: Using streams to handle large files without overwhelming memory usage, making your applications more scalable.

  3. Multer for File Uploads: Implementing secure file upload functionality with proper validation, storage configuration, and error handling.

  4. Static File Serving: Making uploaded files accessible to users through Express's static middleware.

  5. Best Practices: Implementing proper error handling, file validation, and security measures.

Remember These Best Practices:

  • Always use asynchronous file operations in production

  • Implement proper error handling and validation

  • Set appropriate file size limits and type restrictions

  • Use streams for large files

  • Sanitize file names and store files securely

  • Never trust user input - always validate uploaded files

With these concepts and examples, you're well-equipped to handle file operations in your Node.js and Express applications. Whether you're building a simple file upload feature or a complex document management system, these fundamentals will serve as a solid foundation for your development journey.

Start with simple file read/write operations, then gradually incorporate more advanced features like streaming and file uploads as your application grows. Happy coding!