Thinking About File Storage
Thereās a funny thing that happens when you try to rebuild a system you thought you understood.
You start with confidence, convinced youāre just a few lines of code away from āsolvingā the problem. Then, somewhere along the way, the code teaches you that the problem was never what you thought it was.
Thatās exactly what happened to me this week.
I set out to build a clean, Laravel-inspired file storage system in TypeScript. The kind of thing youād package, publish, and forget about. But somewhere between implementing visibility handling, path generators, and file streams, the work stopped being about āfilesā ā and started being about designing for reality.
šŖµ The Illusion of Just āSaving Filesā
When I first wrote:
fs.writeFileSync(path, contents);
I felt like the job was done. Simple, right? Write the file, return the path. On to the next feature.
Except real systems donāt work that way.
In the real world, āsaving a fileā has baggage:
- Where does it live? (Path logic.)
- Who can access it? (Visibility rules.)
- What happens when storage moves to S3? (Abstraction.)
- What happens when the upload is slow, or interrupted? (Resilience.)
- How do you test it all without hardcoding assumptions? (Flexibility.)
Laravel doesnāt hide these problems. It just makes them composable. Thatās the genius. And thatās the part Iāve been chasing in this rewrite.
š Paths Are Strategies, Not Strings
One of the first design dead ends I hit was hardcoded file paths.
You upload an avatar? You might write:
const path = `users/${userId}/avatar.png`;
But this kind of logic spreads like weeds. Before long, itās tangled across your codebase. Worse, it ties your logic to a single assumption: that file paths are static.
Laravelās model taught me something smarter: treat paths as a strategy.
So I wrote:
interface PathGenerator {
generatePath(originalName: string): string;
}
And suddenly, my logic stopped caring about hardcoded assumptions.
Now I could swap strategies:
DatePathGenerator
: store files by upload date.UserPathGenerator
: store files under user directories.- Future-proof: write a
SlugPathGenerator
, aUUIDPathGenerator
, whatever.
The shift is small on the surface. But underneath, it turns your storage system into something that can adapt as requirements evolve ā without rewriting business logic.
š Visibility Isnāt a Folder Name ā Itās a Policy
The next trap I walked into was treating public
and private
as if they were just folders.
But thatās not how Laravel thinks about it.
When you save a file in Laravel:
Storage::put('photos/cat.png', $contents, 'public');
youāre not just writing to a path. Youāre setting an intent: this file can be shared. The implementation decides how thatās enforced ā locally it might be folder-based, on S3 it might be ACLs.
So in my system, I updated my path resolver to reflect that intent:
const targetVisibility = visibility ?? this.defaultVisibility;
return path.join(this.root, targetVisibility, filePath);
Now visibility isnāt an afterthought. Itās baked into the path, enforced by design.
That might seem small. But small design decisions like this prevent big security mistakes later.
š Streams: Where Happy Path Code Goes to Die
And then came the humbling part: testing file streams.
Itās easy to write:
await storage.writeStream('file.txt', readableStream);
and call it a day. But streams donāt always behave. If you donāt handle:
- slow streams,
- interrupted streams,
- unclosed streams,
your tests will hang. Your production code will too.
I wrote tests that simulated:
- normal uploads,
- network-like latency,
- interrupted connections.
And every time the test failed, the design got better. I added proper end
and destroy
calls. I added error listeners. I added timeouts.
You donāt really āgetā streams until you see one hang your test suite at 2AM.
š” The Shift: Laravelās Storage Model Is a Way of Thinking
When I started this project, I thought I was building a file storage system.
What Iāve realized is: Iām actually building a mental model.
Laravelās storage isnāt just about reading and writing files ā itās about designing for:
- change,
- failure,
- policy,
- and most of all: clarity.
Week 2 wasnāt about finishing features. It was about learning to ask better questions:
- What does the code assume?
- What happens when those assumptions break?
- How can design prevent human error, not just handle it?
The code will change. The model will stay.
š„ Whatās Next
This week was all about:
- Path strategies.
- Visibility policies.
- Testing for real-world file behavior.
Next week?
Weāll dig into:
- Signed URLs.
- Slow and interrupted uploads.
- Custom path generators.
- Edge cases that I nearly skipped ā but Iām glad I didnāt.
Because the more I work on this, the more I realize:
It was never about the files.
It was always about designing for the real world.
Did I make a mistake? Please consider Send Email With Subject