
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
andpath
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 theres
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! 🚀