Skip to content

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 File API

Step 1: Create a Storage Backend

Start by creating a filesystem storage backend that writes files to a directory on disk.

ts
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.

ts
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.

ts
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.

ts
// 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) // false

Step 5: Use as an Upload Handler

Combine file-storage with parseFormData to save uploads directly to storage.

ts
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.

ts
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.

ts
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:

ts
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.

ts
// 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

ConceptWhat You Learned
Creating backendscreateFsFileStorage for disk, createMemoryFileStorage for memory
Storing filesstorage.set(key, file) writes a file under a key
Retrieving filesstorage.get(key) returns a File or null
Checking existencestorage.has(key) returns a boolean
Deleting filesstorage.remove(key) deletes a file
Upload handlingCombine with parseFormData to save uploads
Backend swappingSame interface for fs, memory, and S3

Next Steps

Released under the MIT License.