Skip to content

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.

ts
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')         // undefined

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

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

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

Use in a Response

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

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

ts
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

ConceptWhat You Learned
MIME detectiondetectMimeType returns the MIME type for a filename
Content-TypedetectContentType includes charset for text types
CompressibilityisCompressibleMimeType tells you if compression helps
Custom typesdefineMimeType 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

Released under the MIT License.