Local File Storage with TypeScript: A Laravel-Inspired Take
There’s this silent moment that always comes when you’re abstracting infrastructure—storage, queues, databases. It’s the moment right after you get the first thing working, and you ask yourself: What’s this really going to grow into?
That moment hit me like a freight train after I wrote the first few lines of my TypeScript version of Laravel’s file storage system. I had the interface, a LocalStorageDriver
, and a Storage
registry in place. You could .put()
and .get()
files. It worked. But I realized I wasn’t building this for now—I was building it for the next hundred features. And that meant it couldn’t just work; it had to be built to grow.
And that’s where metadata, testing, and error handling came in.
Why Metadata Isn’t Optional
The past version of me didn’t think about file metadata. If a file was there and I could read it, that was good enough. But that version of me had never tried to build a media library, generate signed URLs, or organize files by type.
Metadata isn’t just nice-to-have—it’s the foundation of a real file management system. You need to know what a file is, not just whether it exists.
So I designed a shape:
interface FileMetadata {
path: string;
size: number;
mimeType: string;
visibility: 'public' | 'private';
lastModified: Date;
}
This became the backbone of how I understood files. And it forced my local driver to become smarter. I pulled in fs-extra
for filesystem access, and mime-types
to infer MIME type based on file extension. Then I wrote a getMetadata()
method that pulled it all together.
What changed for me here was realizing that this structure wasn’t just a utility. It was a contract—one that all future drivers (S3, GCS, Azure) would have to honor.
Testing Like the System Depends on It (Because It Does)
Here’s the truth: I don’t write tests because I love writing tests. I write tests because Future Me is a chaotic gremlin who will absolutely break things.
So I wrote a suite of integration tests for the local driver:
- Can it write and read files?
- Does it report the right metadata?
- Does
exists()
behave sensibly? - What happens when the file doesn’t exist?
I used Vitest, but any modern framework will do. What mattered wasn’t the tool—it was the intention:
If this was the only thing I could read about the system in 6 months, would I understand how it’s supposed to behave?
That’s my bar now.
Error Handling That Isn’t an Afterthought
The filesystem is a messy place. Files vanish. Paths are wrong. Disks fill up. If you pretend that won’t happen, your app will crash the first time reality says otherwise.
So I wrapped my reads in try/catch
, and made sure errors came with meaningful messages:
try {
const data = await fs.readFile(fullPath);
} catch (err: any) {
if (err.code === 'ENOENT') {
throw new Error(`File not found: ${path}`);
}
throw err;
}
No vague “Something went wrong.” Give me context. And while we’re at it, I added optional logging, so I could debug without digging into stack traces.
The Takeaway: Make the Boring Parts Boring
I don’t want file storage to be exciting. If I’m thinking about it, something probably went wrong. So my goal is to make this whole system predictable.
- Metadata lets me make smart decisions about files
- Testing protects me from my own future recklessness
- Error handling ensures I hear the crash before it becomes a crater
When you’re building abstractions, the goal isn’t cleverness. It’s trust. I want to trust that .put()
will work the same on local or S3. I want to trust that getMetadata()
gives me the same shape every time.
That trust starts with being just a little obsessive right now.
If you’re building something similar, my advice is: don’t skip this step. Make the boring parts boring. You’ll thank yourself later.
Next up? Cloud drivers. Let’s make this thing fly.
Did I make a mistake? Please consider Send Email With Subject