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.
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 memoryThe 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.
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.
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.
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.
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
| Concept | What You Learned |
|---|---|
| Opening files | openLazyFile(path) returns a LazyFile with deferred content |
| MIME detection | File type is set automatically from the extension |
| Serving files | Combine with createFileResponse for streaming responses |
| Writing files | writeFile(path, file) saves to disk with auto-created directories |
| Upload saving | Combine with parseFormData to persist uploads |
Next Steps
- Learn about LazyFile for byte-range support
- Use file-storage for a backend-agnostic approach
- See the API Reference for all options