
4 Strategic Patterns for the File System Access API: How to Build Desktop-Class File Handling Without the Electron Bloat
Stop forcing users to re-upload files on every edit and learn how to implement persistent, professional-grade file handling directly in the browser.
For decades, we’ve treated the web browser like a high-security prison for files—you can look, but you definitely can't touch anything on the host machine without a cumbersome "Upload/Download" dance. This friction is why we often reach for Electron, bloating our apps with 100MB of Chromium overhead just to let a user click "Ctrl+S" and actually save a file.
The File System Access API changes that. It allows web apps to read, write, and—crucially—stay connected to files and folders on a user's local disk. If you're tired of making users re-upload their work every time they refresh the page, these four strategic patterns will help you bridge the gap between "website" and "desktop application."
1. The "Open-Edit-Save" Loop
Most web apps handle file saving by generating a Blob and triggering a fake download. It's clunky, it clutters the ~/Downloads folder, and it breaks the user's mental model of how a tool should work.
The professional way is to obtain a FileSystemFileHandle. This object is your direct line to the file.
// 1. Pick the file
const [handle] = await window.showOpenFilePicker({
types: [{
description: 'Text Files',
accept: { 'text/plain': ['.txt'] },
}],
});
// 2. Read it
const file = await handle.getFile();
const content = await file.text();
// 3. (Later) Save changes back to the same spot
async function saveFile(handle, contents) {
const writable = await handle.createWritable();
await writable.write(contents);
await writable.close();
}Why this matters: By holding onto that handle variable in your app's state, you can implement an "Auto-save" feature that feels native. No popups, no "Save As" dialogs every five minutes. Just a smooth, invisible sync.
2. Persistent Handles via IndexedDB
Here is the catch: if the user refreshes the page, your JavaScript state is wiped. Normally, that means the user has to go through the file picker *again*. That’s a UX killer.
The trick is that FileSystemHandle objects are serializable. You can't put them in localStorage, but you can store them in IndexedDB.
import { get, set } from 'idb-keyval'; // A tiny wrapper for IndexedDB
async function saveHandleToDisk(handle) {
await set('last-opened-file', handle);
}
async function restoreHandle() {
const handle = await get('last-opened-file');
if (handle) {
// We need to request permission again because
// permissions don't persist across sessions for security
if ((await handle.queryPermission()) === 'granted') {
return handle;
}
if ((await handle.requestPermission()) === 'granted') {
return handle;
}
}
return null;
}The Gotcha: You still have to ask for permission. The browser won't let you silently write to a file just because the user allowed it yesterday. However, clicking a "Restore Session" button is 10x faster than navigating a file explorer.
3. Directory Orchestration for Batch Workflows
If you're building a tool like an image optimizer or a static site generator, asking the user to pick 50 files individually is a war crime. Instead, use showDirectoryPicker.
This gives you a FileSystemDirectoryHandle, which you can iterate over like a standard async collection.
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.png')) {
const file = await entry.getFile();
console.log(`Processing ${file.name}...`);
// Do your logic here
}
}I used this pattern recently for a bulk Markdown editor. The ability to "Open Workspace" makes the browser feel less like a document viewer and more like an IDE (think VS Code for the web). You can even create new files inside that directory handle without further prompts.
4. High-Performance I/O with the Origin Private File System (OPFS)
Sometimes you don't want the user to see the files. Maybe you're porting a C++ library via WebAssembly, or you're running a SQLite database in the browser and you need raw, screaming-fast disk performance.
The Origin Private File System is a storage endpoint that is private to your origin. It’s not visible in the user's Finder or Windows Explorer, but it uses the same File System Access API.
Inside a Web Worker, you can access the "Sync Access Handle," which is significantly faster because it bypasses the main thread's asynchronous overhead.
// Inside a Web Worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('data.bin', { create: true });
// This is the "Turbo" mode
const accessHandle = await fileHandle.createSyncAccessHandle();
const buffer = new DataView(new ArrayBuffer(1024));
const readSize = accessHandle.read(buffer, { at: 0 });
// Always close when done to release the lock!
accessHandle.close();The Trade-off: OPFS is strictly for performance and internal app data. If you want the user to be able to double-click a file in their documents folder, stick to the patterns above. But if you're building a heavy-duty app like Photoshop on the Web, OPFS is your best friend.
Security and the "User Gesture"
The browser is still a cautious gatekeeper. You cannot trigger showOpenFilePicker on page load; it must result from a user gesture (like a click).
Also, keep in mind that this API is currently a "Chromium-first" feature. While it’s making its way into other browsers (Safari has partial support, Firefox is lagging), always wrap your code in a feature check:
if ('showOpenFilePicker' in window) {
// We're in the future!
} else {
// Fall back to <input type="file">
}Wrapping Up
Electron has its place, but for many productivity tools, it’s overkill. By combining FileSystemHandle persistence via IndexedDB with the direct writing capabilities of the File System Access API, you can build tools that feel fast, integrated, and respectful of the user's local environment.
The web isn't just for viewing content anymore—it's for creating it, saving it, and actually owning the data on your own hard drive.


