>_>_
Creating Your Own ExpressJS from Scratch (Part 5) - Serving Static Files

Creating Your Own ExpressJS from Scratch (Part 5) - Serving Static Files

So far, our framework can handle routing, middleware, modular routers, and controllers — but it can’t serve files yet! In this part, we’ll implement support for serving static files like .html, .css, .json, or images.

We’ll do this by combining recursive folder traversal and Node.js Streams to efficiently pipe files to the client.


🧪 What You Will Learn

  • How to use fs and path to traverse directories
  • How to stream files to the HTTP response
  • How to register routes automatically for each file

📁 Reading Files Recursively

To serve static files, we must locate them — even if they're nested inside folders.

1. Import required modules

const { readdir } = require('fs/promises');
const { statSync, createReadStream } = require('fs');
const path = require('path');
const { pipeline } = require('stream/promises');

These provide directory traversal, file stream reading, and safe piping.

2. Recursively discover all files

async function* getAllStaticFiles(folder) {
  const files = await readdir(folder);
  for (const file of files) {
    const absolutePath = path.join(folder, file);
    if (statSync(absolutePath).isDirectory()) {
      yield* getAllStaticFiles(absolutePath);
    } else {
      yield absolutePath;
    }
  }
}

This generator yields every file path under the target folder.


🚀 Serving Static Files with Dynamic Routes

Now let’s write a static() function to register routes for every discovered file.

3. Define the static function

const static = async (folderPath) => {
  let folderRelative = folderPath.replace('./', '');

  for await (const file of getAllStaticFiles(folderPath)) {
    const pathWithoutBase = file.replace(folderRelative, '');

    get(pathWithoutBase, async (req, res) => {
      const relativePath = path.join(__dirname, '..', file);
      const fileStream = createReadStream(relativePath);
      res.setHeader('Content-Type', file.split('.').filter(Boolean).slice(1).join('.'));
      return await pipeline(fileStream, res);
    });
  }
};
  • We map each file path to a GET route
  • We use a readable stream to read the file
  • We pipeline() the stream into the res object (which is a writable stream!)
  • We set the content type based on the file extension

🔗 Exposing the Function

Update your exported object in app.js to include:

return {
  run,
  get,
  post,
  patch,
  put,
  del,
  use,
  useAll,
  useRouter,
  static,
};

🧪 Testing the Static File Server

Let’s try it in index.js:

app.static('./files');

const start = async () => {
  app.run(3000);
};

start();

Suppose your directory structure is:

files/
└── folder1/
    └── file.json

Contents of file.json:

{
  "test": "test"
}

Start your server and open:

GET http://localhost:3000/folder1/file.json

✅ You should see the JSON content streamed directly to your browser or API client.


📚 Summary

  • You implemented a static file server using Streams
  • Each file is exposed via a dynamic route
  • You now support both APIs and assets — just like a real-world framework

▶️ Coming Up Next

In Part 6, we’ll implement our own body-parser middleware to handle incoming JSON and form data requests.

See you in the final part of the series! 🚀