Skip to content

FS Tutorial

This tutorial shows you how to use the fs package to open files from disk as web-standard File objects, write files to disk, and serve files as HTTP responses.

Prerequisites

  • A Remix V3 project running on Node.js (or another runtime with filesystem access)

Step 1: Open a File from Disk

Use openLazyFile to create a LazyFile from a path on disk. The returned file has its metadata (name, type, size, lastModified) populated from the filesystem, but content is not read until accessed.

ts
import { openLazyFile } from 'remix/fs'

let file = await openLazyFile('./public/images/hero.jpg')

console.log(file.name)         // 'hero.jpg'
console.log(file.type)         // 'image/jpeg'
console.log(file.size)         // 524288
console.log(file.lastModified) // 1704067200000

// Content is still on disk -- not in memory

The MIME type is detected from the file extension. A .jpg file gets image/jpeg, a .css file gets text/css, and so on.

Step 2: Serve a File as an HTTP Response

Combine openLazyFile with createFileResponse to serve files efficiently. The file content streams from disk to the client without buffering the whole file in memory.

ts
import { openLazyFile } from 'remix/fs'
import { createFileResponse } from 'remix/response/file'

export async function loader({ params }: { params: { filename: string } }) {
  try {
    let file = await openLazyFile(`./public/downloads/${params.filename}`)
    return createFileResponse(file)
  } catch {
    return new Response('File not found', { status: 404 })
  }
}

Step 3: Write a File to Disk

Use writeFile to save a File or Blob to a path. Parent directories are created automatically.

ts
import { writeFile } from 'remix/fs'

let file = new File(['Hello, world!'], 'greeting.txt', {
  type: 'text/plain',
})

// Parent directories are created if they don't exist
await writeFile('./data/greetings/greeting.txt', file)

Step 4: Save Uploaded Files to Disk

Combine writeFile with form data parsing to save uploaded files.

ts
import { parseFormData, FileUpload } from 'remix/form-data-parser'
import { writeFile } from 'remix/fs'

export async function action({ request }: { request: Request }) {
  let formData = await parseFormData(request, {}, async (fileUpload) => {
    let path = `./uploads/${Date.now()}-${fileUpload.name}`
    let file = new File([await fileUpload.arrayBuffer()], fileUpload.name, {
      type: fileUpload.type,
    })
    await writeFile(path, file)
    return path
  })

  let savedPath = formData.get('document') as string
  return Response.json({ savedPath })
}

Step 5: Read, Transform, and Write Back

Open a file, process its content, and write the result to a new location.

ts
import { openLazyFile, writeFile } from 'remix/fs'

// Read a JSON config file
let configFile = await openLazyFile('./config/defaults.json')
let config = JSON.parse(await configFile.text())

// Modify the config
config.version = '2.0.0'
config.updatedAt = new Date().toISOString()

// Write the updated config
let updated = new File(
  [JSON.stringify(config, null, 2)],
  'defaults.json',
  { type: 'application/json' },
)
await writeFile('./config/defaults.json', updated)

Summary

ConceptWhat You Learned
Opening filesopenLazyFile(path) returns a LazyFile with deferred content
MIME detectionFile type is set automatically from the extension
Serving filesCombine with createFileResponse for streaming responses
Writing fileswriteFile(path, file) saves to disk with auto-created directories
Upload savingCombine with parseFormData to persist uploads

Next Steps

Released under the MIT License.