· deepdives · 7 min read
Understanding the File System Access API: Web Development's New Frontier
Learn how the File System Access API lets web apps interact directly with a user's files and directories. This guide covers capabilities, security, code examples, browser support, real-world use cases, fallbacks, and best practices for building modern, file-first web apps.

Why the File System Access API matters
Until recently, web apps were limited when dealing with files: you could read files via or drag-and-drop, and you could prompt downloads to export data. But you couldn’t open, edit, and save files back to the user’s file system in-place like a native app.
The File System Access API (sometimes referred to historically as the Native File System API) changes that. It gives web applications a standardized way to request access to files and directories, read and write them, and - with user permission - remember those access handles for a smoother experience later. This capability enables a whole new class of web apps: editors, IDEs, media tools, batch processors, and local-first apps that behave like desktop software while staying in the browser.
References: MDN docs and spec are good starting points: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API and the spec at https://w3c.github.io/file-system-access/
Core concepts and primitives
- FileSystemFileHandle: a handle that represents a file in the user’s file system. You use it to request the file object and to create writable streams.
- FileSystemDirectoryHandle: a handle to a directory; it enables reading directory entries, creating files or directories, and iterating entries.
- showOpenFilePicker(), showSaveFilePicker(), showDirectoryPicker(): top-level methods that open native file/folder pickers. They return handles or arrays of handles.
- createWritable(): called on a FileSystemFileHandle to obtain a writable stream that supports atomic writing.
- getFile(): called on a FileSystemFileHandle to obtain a File object for reading.
These primitives are intentionally handle-based: the app doesn’t get raw paths; instead it receives capability-bound handles that carry user-granted access.
Quick examples
Read a file selected by the user:
// Single file open
const [fileHandle] = await window.showOpenFilePicker({
types: [
{
description: 'Text files',
accept: { 'text/plain': ['.txt', '.md'] },
},
],
});
const file = await fileHandle.getFile();
const text = await file.text();
console.log(text);
Save a file (Save As flow):
const fileHandle = await window.showSaveFilePicker({
suggestedName: 'untitled.txt',
types: [
{
description: 'Text files',
accept: { 'text/plain': ['.txt'] },
},
],
});
const writable = await fileHandle.createWritable();
await writable.write('Hello, world!');
await writable.close();
Write atomically (overwrite safely): createWritable creates a temporary file and commits when closed - this helps avoid data corruption for important files.
Pick a directory and iterate entries:
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle.entries()) {
console.log(name, handle.kind); // 'file' or 'directory'
}
Persisting and re-using a handle:
// store handles in IndexedDB using the structured clone algorithm
await db.put('fileHandle', fileHandle);
// later restore and check permissions
const storedHandle = await db.get('fileHandle');
const perm = await storedHandle.requestPermission({ mode: 'readwrite' });
if (perm === 'granted') {
/* use handle */
}
Note: handles can be stored/serialized using IndexedDB (they support structured clone) and later restored, but re-check permissions before accessing.
Security model and permissions
Security is central. The API never exposes raw file paths to the web app. Instead, it uses capability handles granted directly by the user via native pickers. Permission decisions are explicit and scoped:
- Temporary permissions: granted for the current session only.
- Persistent permissions: some browsers permit “persisted” access to handles stored in IndexedDB, but the user may still be prompted.
- Permission modes: ‘read’ or ‘readwrite’. You should always request the minimal permission necessary.
You can check and request permissions programmatically using the handle.requestPermission() and handle.queryPermission() methods. Always handle denied permissions gracefully.
More on this: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API#security
Browser support and progressive enhancement
Support is improving but not universal. Chromium-based browsers (Chrome, Edge) have robust support. Firefox and Safari historically lag; mobile support varies.
Use a feature detection guard:
if ('showOpenFilePicker' in window) {
// use the File System Access API
} else {
// fallback (input[type=file], server uploads, etc.)
}
Caniuse provides an up-to-date compatibility table: https://caniuse.com/mdn-api_file_system_access
Fallback strategies
When the API is unavailable, provide fallbacks so users still accomplish tasks:
- Reading: use or drag-and-drop to get File objects.
- Saving: create downloadable blobs and trigger a download with an anchor ([URL.createObjectURL(blob)] and a link with download attribute).
- Directory operations: simulate directory upload with input type=“file” webkitdirectory (non-standard) or request the user to upload a ZIP.
Progressive enhancement is the path: prefer the File System Access API where available, and fall back gracefully.
Real-world use cases
- In-browser code editors and IDEs (e.g., edit files directly from disk, save changes back to original files).
- Image, audio, and video editors that open local assets and overwrite originals after edits.
- Spreadsheet and CSV editors that can batch-process files in directories.
- Batch file manipulators (rename, convert, compress multiple files inside a folder selected by the user).
- Local-first apps and PWAs that offer offline editing and sync later to cloud services while keeping local persistence.
- Backup and restore utilities that read and write large files using streaming for performance.
Examples from the ecosystem include browser-based development environments and complex editors that now match desktop experiences for file I/O.
Best practices
- Request the least privilege: ask only for read or readwrite as needed.
- Use createWritable() for atomic writes to avoid corruption; close the stream to commit.
- For large files, use streams and chunked writes rather than loading entire files into memory.
- Persist handles in IndexedDB if your app workflow benefits from reopening files/directories later; always re-check permissions and user intent.
- Provide clear messaging about why you need access and what will happen to the files.
- Gracefully handle permission denials and cancellations.
- Test on multiple platforms - desktop and mobile - and implement fallbacks.
Performance considerations
- Avoid reading gigantic files into memory. Use readable streams and process in chunks.
- When writing large outputs, prefer createWritable + write() with well-sized chunks.
- Be mindful of I/O cost on slower drives or network-mounted file systems; provide progress feedback for long operations.
Common pitfalls and how to avoid them
- Assuming persistent access: even if a handle is stored, permissions can be revoked by the user or not granted on subsequent sessions. Always call requestPermission or queryPermission and handle failures.
- Not using atomic writes: writing directly without createWritable can risk file corruption. Use the API’s writable streams.
- Cross-browser differences: behavior and UX (prompts, persistent permission strategies) differ across engines. Test widely.
- Mobile constraints: the file pickers and persistent storage support on mobile browsers is more limited - design for fallback flows.
Example: a minimal in-browser file editor
This sketch shows opening, editing, and saving a text file. It demonstrates the typical flow and UI states you should account for.
async function openAndEditFile() {
const [handle] = await window.showOpenFilePicker({
multiple: false,
types: [{ accept: { 'text/plain': ['.txt'] } }],
});
const file = await handle.getFile();
const text = await file.text();
// populate editor
editor.value = text;
// Save button
saveBtn.onclick = async () => {
const writable = await handle.createWritable();
await writable.write(editor.value);
await writable.close();
alert('Saved');
};
}
UX notes: show spinners while reading/writing, disable controls during I/O, show human-friendly errors if permissions fail or the user moves/deletes the file outside the app.
When not to use the File System Access API
- When you need to work purely server-side or your use case already relies on secure cloud storage with strict policies.
- When the user environment is known to lack support and fallbacks would be too limited for the app’s core functionality.
- If the app design requires fine-grained OS-level integration not exposed by the web API.
The future and closing thoughts
The File System Access API significantly elevates what web apps can do offline and locally. It’s an important step toward parity with desktop apps, enabling richer experiences without forcing users to install native software.
Adopt the API where it makes sense, follow best practices for security and UX, and always include fallbacks. As browser support expands, expect more powerful, file-first web apps to appear - tools that blur the line between web and desktop workflows.
Further reading:
- MDN: File System Access API - https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
- Spec: File System Access - https://w3c.github.io/file-system-access/
- Compatibility: Can I Use entry - https://caniuse.com/mdn-api_file_system_access