File Storage Tutorial
This tutorial walks through using the file-storage package to manage uploaded files. You will learn how to create storage backends, store and retrieve files, delete files, and swap between backends for different environments.
Prerequisites
- A Remix V3 project
- Basic familiarity with the
FileAPI
Step 1: Create a Storage Backend
Start by creating a filesystem storage backend that writes files to a directory on disk.
import { createFsFileStorage } from 'remix/file-storage/fs'
let storage = createFsFileStorage({
directory: './uploads',
})The directory is created automatically if it does not exist. File keys map to paths inside this directory -- a key of avatars/user-1.jpg writes to ./uploads/avatars/user-1.jpg.
Step 2: Store a File
Use set(key, file) to store a File object. If a file already exists at that key, it is overwritten.
let file = new File(['profile data'], 'profile.json', {
type: 'application/json',
})
await storage.set('users/123/profile.json', file)You can store files from any source -- form uploads, API responses, or files you create in code.
Step 3: Retrieve a File
Use get(key) to retrieve a file. It returns a File object or null if the key does not exist.
let file = await storage.get('users/123/profile.json')
if (file) {
console.log(file.name) // 'profile.json'
console.log(file.type) // 'application/json'
let content = await file.text()
console.log(content) // 'profile data'
} else {
console.log('File not found')
}The returned File object uses lazy reading -- the file content is not loaded from disk until you call text(), arrayBuffer(), or stream().
Step 4: Check Existence and Delete
Use has(key) to check if a file exists without reading its content. Use remove(key) to delete a file.
// Check if a file exists
let exists = await storage.has('users/123/profile.json')
console.log(exists) // true
// Delete the file
await storage.remove('users/123/profile.json')
// Confirm deletion
exists = await storage.has('users/123/profile.json')
console.log(exists) // falseStep 5: Use as an Upload Handler
Combine file-storage with parseFormData to save uploads directly to storage.
import { parseFormData, FileUpload } from 'remix/form-data-parser'
import { createFsFileStorage } from 'remix/file-storage/fs'
let storage = createFsFileStorage({ directory: './uploads' })
export async function action({ request }: { request: Request }) {
let formData = await parseFormData(request, {}, async (fileUpload) => {
let key = `photos/${Date.now()}-${fileUpload.name}`
let file = new File([await fileUpload.arrayBuffer()], fileUpload.name, {
type: fileUpload.type,
})
await storage.set(key, file)
return key
})
let photoKey = formData.get('photo') as string
return Response.json({ photoKey })
}Step 6: Serve Files from Storage
Retrieve a stored file and return it as an HTTP response.
import { createFileResponse } from 'remix/response/file'
export async function loader({ params }: { params: { key: string } }) {
let file = await storage.get(params.key)
if (!file) {
return new Response('Not Found', { status: 404 })
}
return createFileResponse(file)
}Step 7: Switch Backends by Environment
Because all backends share the same FileStorage interface, you can swap implementations based on the environment.
import type { FileStorage } from 'remix/file-storage'
import { createFsFileStorage } from 'remix/file-storage/fs'
import { createMemoryFileStorage } from 'remix/file-storage/memory'
let storage: FileStorage
if (process.env.NODE_ENV === 'test') {
// Use in-memory storage for fast, isolated tests
storage = createMemoryFileStorage()
} else {
// Use filesystem storage in development and production
storage = createFsFileStorage({ directory: './uploads' })
}
export { storage }For cloud deployments, switch to S3:
import { createS3FileStorage } from 'remix/file-storage-s3'
let storage = createS3FileStorage({
bucket: process.env.S3_BUCKET!,
region: process.env.AWS_REGION!,
})Step 8: Organize Files with Key Prefixes
Use key prefixes to organize files into logical groups, similar to directories.
// Store user avatars under a user-specific prefix
await storage.set(`users/${userId}/avatar.jpg`, avatarFile)
// Store document attachments under a document prefix
await storage.set(`documents/${docId}/attachment-1.pdf`, pdfFile)
await storage.set(`documents/${docId}/attachment-2.pdf`, pdfFile2)Summary
| Concept | What You Learned |
|---|---|
| Creating backends | createFsFileStorage for disk, createMemoryFileStorage for memory |
| Storing files | storage.set(key, file) writes a file under a key |
| Retrieving files | storage.get(key) returns a File or null |
| Checking existence | storage.has(key) returns a boolean |
| Deleting files | storage.remove(key) deletes a file |
| Upload handling | Combine with parseFormData to save uploads |
| Backend swapping | Same interface for fs, memory, and S3 |
Next Steps
- Use file-storage-s3 for cloud storage
- Serve files efficiently with response helpers
- See the API Reference for all options