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.