MIME Tutorial
This tutorial covers common MIME-related tasks: detecting types from filenames, setting response headers, deciding when to compress, and adding custom types.
Prerequisites
- A Remix V3 project
Step 1: Detect a MIME Type
Use detectMimeType to look up a MIME type by file extension. It works with bare filenames or full paths.
import { detectMimeType } from 'remix/mime'
detectMimeType('photo.jpg') // 'image/jpeg'
detectMimeType('archive.tar.gz') // 'application/gzip'
detectMimeType('/uploads/doc.pdf') // 'application/pdf'
detectMimeType('unknown.xyz') // undefinedThe function returns undefined for unrecognized extensions. Always handle this case when setting headers.
Step 2: Set Content-Type Headers
Use detectContentType to get a complete Content-Type header value. It adds charset=utf-8 for text-based formats automatically.
import { detectContentType } from 'remix/mime'
import { openLazyFile } from 'remix/fs'
export async function loader({ params }: { params: { file: string } }) {
let file = await openLazyFile(`./public/${params.file}`)
let contentType = detectContentType(params.file) ?? 'application/octet-stream'
return new Response(file.stream(), {
headers: {
'Content-Type': contentType,
'Content-Length': String(file.size),
},
})
}The difference between detectMimeType and detectContentType:
| Function | 'index.html' | 'photo.png' |
|---|---|---|
detectMimeType | 'text/html' | 'image/png' |
detectContentType | 'text/html; charset=utf-8' | 'image/png' |
Text types get a charset parameter. Binary types do not.
Step 3: Decide When to Compress
Use isCompressibleMimeType to check whether a response body would benefit from compression. This is useful when building middleware or deciding whether to apply gzip.
import { isCompressibleMimeType, detectMimeType } from 'remix/mime'
function shouldCompress(filename: string): boolean {
let type = detectMimeType(filename)
if (!type) return false
return isCompressibleMimeType(type)
}
shouldCompress('app.js') // true -- JavaScript is text-based
shouldCompress('style.css') // true -- CSS is text-based
shouldCompress('data.json') // true -- JSON is text-based
shouldCompress('photo.jpg') // false -- JPEG is already compressed
shouldCompress('archive.zip') // false -- ZIP is already compressedUse in a Response
import { detectMimeType, isCompressibleMimeType } from 'remix/mime'
import { compressResponse } from 'remix/response/compress'
export async function loader({ request }: { request: Request }) {
let body = await fetchData()
let response = Response.json(body)
let type = response.headers.get('Content-Type') ?? ''
let mimeType = type.split(';')[0].trim()
if (isCompressibleMimeType(mimeType)) {
return compressResponse(response, request)
}
return response
}Step 4: Register Custom MIME Types
If your application uses file extensions that are not in the built-in database, register them with defineMimeType.
import { defineMimeType, detectMimeType } from 'remix/mime'
// Register custom types
defineMimeType('.mdx', 'text/mdx')
defineMimeType('.wasm', 'application/wasm')
defineMimeType('.avif', 'image/avif')
// Now detection works for these extensions
detectMimeType('page.mdx') // 'text/mdx'
detectMimeType('app.wasm') // 'application/wasm'
detectMimeType('photo.avif') // 'image/avif'Call defineMimeType at application startup (e.g., in your server entry file) so the types are available everywhere.
Step 5: Build a Static File Server
Combine MIME detection with filesystem utilities to serve static files with correct headers.
import { openLazyFile } from 'remix/fs'
import { detectContentType, isCompressibleMimeType } from 'remix/mime'
import { compressResponse } from 'remix/response/compress'
export async function loader({
request,
params,
}: {
request: Request
params: { '*': string }
}) {
let path = `./public/${params['*']}`
let file
try {
file = await openLazyFile(path)
} catch {
return new Response('Not Found', { status: 404 })
}
let contentType = detectContentType(file.name) ?? 'application/octet-stream'
let response = new Response(file.stream(), {
headers: {
'Content-Type': contentType,
'Content-Length': String(file.size),
'Cache-Control': 'public, max-age=86400',
},
})
// Compress text-based files
let mimeType = contentType.split(';')[0].trim()
if (isCompressibleMimeType(mimeType)) {
return compressResponse(response, request)
}
return response
}Summary
| Concept | What You Learned |
|---|---|
| MIME detection | detectMimeType returns the MIME type for a filename |
| Content-Type | detectContentType includes charset for text types |
| Compressibility | isCompressibleMimeType tells you if compression helps |
| Custom types | defineMimeType registers new extensions |
Next Steps
- Use response helpers that handle MIME types automatically
- See the API Reference for the full list of built-in types