Skip to main content

Guide

file-type vs mime-types vs mmmagic 2026

Compare file-type, mime-types, and mmmagic for detecting file types and MIME types in Node.js. Magic bytes vs extension-based detection, security now.

·PkgPulse Team·
0

TL;DR

file-type detects file types by reading the actual binary content (magic bytes) — it doesn't trust the file extension or Content-Type header, making it the secure choice for user-uploaded files. mime-types maps file extensions and MIME strings — fast lookup, no file reading, zero dependencies, ideal for serving static files or generating correct Content-Type headers. mmmagic is a Node.js binding to the libmagic library (requires native compilation) — powerful but heavy. In 2026: use file-type for any uploaded file validation, mime-types for extension→MIME lookups, and skip mmmagic unless you need libmagic's full detection capabilities.

Key Takeaways

  • file-type: ~8M weekly downloads — reads binary magic bytes, ESM-only since v19, async, secure
  • mime-types: ~85M weekly downloads — extension ↔ MIME mapping, synchronous, zero dependencies
  • mmmagic: ~500K weekly downloads — native libmagic binding, requires build tools, powerful but complex
  • Never trust file extensions or Content-Type headers for security — always check magic bytes
  • file-type detects 100+ formats: images, audio, video, archives, documents, executables
  • mime-types is extension-based — mime-types.lookup("photo.jpg") returns "image/jpeg"

Extension vs Magic Bytes Detection

Extension-based (mime-types):
  "photo.jpg" → "image/jpeg"
  "file.pdf"  → "application/pdf"
  ❌ Problem: anyone can rename "malware.exe" to "photo.jpg"
  ✓ Use for: Content-Type headers, file serving, non-security contexts

Magic bytes detection (file-type):
  Reads first few bytes of actual file content:
  FF D8 FF → JPEG image
  25 50 44 46 → PDF (%PDF)
  50 4B 03 04 → ZIP (or docx/xlsx)
  MZ → Windows executable (PE format)
  ✓ Use for: validating user uploads, rejecting disguised malware

file-type

file-type — detect files by content:

Basic detection

import { fileTypeFromFile, fileTypeFromBuffer, fileTypeFromStream } from "file-type"

// From a file path:
const result = await fileTypeFromFile("/uploads/user-file.jpg")
// { ext: "jpg", mime: "image/jpeg" }

// From a Buffer:
const buffer = fs.readFileSync("/uploads/user-file.jpg")
const fromBuffer = await fileTypeFromBuffer(buffer)
// { ext: "jpg", mime: "image/jpeg" }

// From a ReadableStream (memory-efficient for large files):
const stream = fs.createReadStream("/uploads/large-video.mp4")
const fromStream = await fileTypeFromStream(stream)
// { ext: "mp4", mime: "video/mp4" }

// Unknown file type:
const unknown = await fileTypeFromBuffer(Buffer.from("just some text"))
// undefined — not a recognized binary format

File upload validation middleware

import express from "express"
import multer from "multer"
import { fileTypeFromBuffer } from "file-type"
import path from "path"

const upload = multer({ storage: multer.memoryStorage() })

const ALLOWED_MIME_TYPES = new Set([
  "image/jpeg",
  "image/png",
  "image/gif",
  "image/webp",
  "application/pdf",
])

// Maximum file sizes by type:
const MAX_SIZE: Record<string, number> = {
  "image/jpeg": 10 * 1024 * 1024,   // 10MB
  "image/png": 10 * 1024 * 1024,
  "application/pdf": 50 * 1024 * 1024,  // 50MB
}

async function validateUpload(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  if (!req.file) {
    return res.status(400).json({ error: "No file uploaded" })
  }

  // Check ACTUAL content, not the extension or Content-Type header:
  const detected = await fileTypeFromBuffer(req.file.buffer)

  if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {
    return res.status(400).json({
      error: "Invalid file type",
      detected: detected?.mime ?? "unknown",
      allowed: [...ALLOWED_MIME_TYPES],
    })
  }

  // Check size for specific type:
  const maxSize = MAX_SIZE[detected.mime]
  if (maxSize && req.file.size > maxSize) {
    return res.status(400).json({
      error: `File too large for ${detected.mime}: max ${maxSize / 1024 / 1024}MB`,
    })
  }

  // Normalize the filename extension to match actual content:
  req.file.mimetype = detected.mime
  req.file.originalname = `${path.parse(req.file.originalname).name}.${detected.ext}`

  next()
}

app.post("/upload", upload.single("file"), validateUpload, async (req, res) => {
  // File is validated — safe to process
  const file = req.file!
  // ... save to storage, process, etc.
  res.json({ success: true, type: file.mimetype })
})

Detecting specific formats

import { fileTypeFromBuffer } from "file-type"

// file-type detects 100+ formats including:
// Images: jpg, png, gif, webp, avif, heic, bmp, tiff, ico, svg (limited)
// Video: mp4, mov, avi, mkv, webm, flv, wmv
// Audio: mp3, flac, wav, ogg, aac, m4a, opus
// Archives: zip, gz, bz2, xz, 7z, rar, tar
// Documents: pdf, docx, xlsx, pptx (these are ZIP-based)
// Executables: exe, elf, dylib
// Fonts: ttf, otf, woff, woff2
// Databases: sqlite

// Detect SVG (text-based, needs separate check):
async function detectFileType(buffer: Buffer): Promise<string> {
  const binary = await fileTypeFromBuffer(buffer)
  if (binary) return binary.mime

  // SVG is XML — file-type can't detect it via magic bytes
  const text = buffer.toString("utf8", 0, 1000)
  if (text.includes("<svg") || text.includes("<?xml")) return "image/svg+xml"
  if (text.trim().startsWith("{") || text.trim().startsWith("[")) return "application/json"

  return "text/plain"
}

ESM-only note (v19+)

// file-type v19+ is ESM-only — cannot use require():
// ❌ const { fileTypeFromFile } = require("file-type")
// ✅ import { fileTypeFromFile } from "file-type"

// For CommonJS projects — use dynamic import or pin to v18:
// Option 1 — dynamic import:
const { fileTypeFromBuffer } = await import("file-type")

// Option 2 — package.json (pin to last CJS version):
// "file-type": "^18.7.0"

// package.json ESM:
// { "type": "module" }

mime-types

mime-types — extension ↔ MIME type mapping:

Basic usage

import mime from "mime-types"

// Extension to MIME type:
mime.lookup("photo.jpg")           // "image/jpeg"
mime.lookup("archive.tar.gz")      // "application/gzip"
mime.lookup("document.pdf")        // "application/pdf"
mime.lookup("index.ts")            // "video/mp2t" (TypeScript maps to MPEG-TS)
mime.lookup("unknown.xyz")         // false (not found)

// MIME type to extension (returns most common extension):
mime.extension("image/jpeg")       // "jpeg"
mime.extension("text/html")        // "html"
mime.extension("application/json") // "json"
mime.extension("unknown/type")     // false

// Get character set for MIME type:
mime.charset("text/html")          // "UTF-8"
mime.charset("image/jpeg")         // false (binary, no charset)

// Content-Type header (includes charset if applicable):
mime.contentType("html")           // "text/html; charset=utf-8"
mime.contentType("json")           // "application/json; charset=utf-8"
mime.contentType("image/jpeg")     // "image/jpeg"

Express static file serving

import express from "express"
import mime from "mime-types"
import fs from "fs"
import path from "path"

const app = express()

// Serve files with correct Content-Type:
app.get("/files/:filename", (req, res) => {
  const filename = req.params.filename
  const filePath = path.join(__dirname, "uploads", filename)

  if (!fs.existsSync(filePath)) {
    return res.status(404).send("Not found")
  }

  // Determine Content-Type from extension:
  const contentType = mime.contentType(path.extname(filename)) || "application/octet-stream"
  res.setHeader("Content-Type", contentType)

  // Stream the file:
  fs.createReadStream(filePath).pipe(res)
})

Generate download headers

import mime from "mime-types"

function getDownloadHeaders(filename: string) {
  const contentType = mime.contentType(filename) || "application/octet-stream"

  return {
    "Content-Type": contentType,
    "Content-Disposition": `attachment; filename="${filename}"`,
  }
}

// Usage:
const headers = getDownloadHeaders("report.pdf")
// {
//   "Content-Type": "application/pdf",
//   "Content-Disposition": "attachment; filename=\"report.pdf\""
// }

Custom MIME types

import mime from "mime-types"

// mime-types uses the mime-db package — all registered IANA types + extras
// For custom types, use the `mime` package (by @broofa):
import Mime from "mime"

const customMime = new Mime({
  "application/x-pkgpulse": ["pkgpulse", "pkgp"],
  "application/x-custom-format": ["custom"],
})

customMime.getType("file.pkgpulse")  // "application/x-pkgpulse"
customMime.getExtension("application/x-pkgpulse")  // "pkgpulse"

mmmagic

mmmagic — Node.js bindings to libmagic:

Basic usage

import mmm from "mmmagic"
const { Magic, MAGIC_MIME_TYPE } = mmm

const magic = new Magic(MAGIC_MIME_TYPE)

// Detect file type (callback-based API):
magic.detectFile("/uploads/user-file.jpg", (err, result) => {
  if (err) throw err
  console.log(result)  // "image/jpeg"
})

// Or promisify:
import { promisify } from "util"
const detectFile = promisify(magic.detectFile.bind(magic))

const mimeType = await detectFile("/uploads/user-file.jpg")
// "image/jpeg"

Why mmmagic is less common in 2026

Requires:
  - libmagic system library (brew install libmagic / apt-get install libmagic-dev)
  - Python + build tools (node-gyp compilation)
  - Doesn't work in serverless/edge environments

vs file-type:
  - Pure JavaScript (no native deps)
  - Works in Deno, Bun, Cloudflare Workers
  - Actively maintained, ESM, modern API
  - Detects all the common formats you need

Use mmmagic if:
  - You need libmagic's extended detection (e.g., specific text encoding detection)
  - You're on a server where native deps are acceptable
  - You need MIME detection with encoding flags (MAGIC_MIME_ENCODING)

Feature Comparison

Featurefile-typemime-typesmmmagic
Detection methodMagic bytes (secure)Extension lookuplibmagic (native)
Security for uploads
No native deps
Async❌ (sync)
Extension → MIME
MIME → extension
Edge/serverless
Bundle size~16KB~220KB (mime-db)Native
TypeScript⚠️ @types
Weekly downloads~8M~85M~500K

When to Use Each

Choose file-type if:

  • Validating user-uploaded files on the server
  • Need to detect actual file content regardless of filename/extension
  • Working in serverless, Deno, Bun, or Cloudflare Workers
  • Building a file upload pipeline where security matters

Choose mime-types if:

  • Setting Content-Type headers for file downloads or API responses
  • Converting file extensions to MIME types for static file serving
  • Need a quick, synchronous, zero-dep lookup
  • Not dealing with untrusted files (serving your own known assets)

Choose mmmagic if:

  • Need libmagic's full feature set (text encoding detection, MIME flags)
  • Running on a traditional server where native deps are OK
  • Migrating from a system that already uses libmagic

Security note:

// ❌ WRONG — trusts user-controlled data:
const fileExt = path.extname(uploadedFile.originalname)
const mimeType = mime.lookup(fileExt)  // Easy to bypass!
if (!allowedTypes.includes(mimeType)) reject()

// ✅ CORRECT — checks actual content:
const detected = await fileTypeFromBuffer(uploadedFile.buffer)
if (!detected || !allowedTypes.includes(detected.mime)) reject()

Security-First File Upload Architecture

Building a secure file upload pipeline requires more than just checking magic bytes — it requires defense in depth. After detecting the MIME type with file-type, you should also validate the file's structure for your expected format. A file that claims to be a JPEG via magic bytes but is actually a polyglot file (valid JPEG and valid PHP simultaneously) can still be dangerous if served from your domain. For images specifically, re-encoding the file through a library like sharp strips any embedded metadata or secondary payloads and produces a clean output from the decoded pixel data — this is called image sanitization and is the gold standard for user avatar and photo uploads. Store uploaded files outside the web root or behind a signed URL system that prevents direct access, and never serve uploaded files with Content-Disposition: inline unless you have verified the content type is safe for browser rendering.

ESM Migration Considerations for file-type

file-type v19+ moving to ESM-only is part of Sindre Sorhus's broader strategy of publishing ESM-only packages. This caused significant pain for CommonJS projects but is increasingly manageable in 2026. If your Node.js project uses "type": "module" in package.json, you're already ESM and file-type works natively. For CommonJS projects, the dynamic import workaround (const { fileTypeFromBuffer } = await import("file-type")) works in async contexts — which file type detection always is — making the compatibility issue largely moot in practice. The bigger challenge is Jest testing environments, which historically used CommonJS. Jest's new ESM experimental mode and Vitest (which is ESM-native) both work with file-type v19+ without workarounds. If you're blocked on this, pin to file-type v18 — it's the last CJS-compatible release and still receives security patches.

Performance Considerations at Scale

For high-volume file processing, the performance characteristics of each approach matter. mime-types lookups are synchronous and O(1) — essentially a hash table lookup that completes in microseconds. file-type reads only the first few hundred bytes of a file (the magic bytes location varies by format: JPEG headers are in the first 3 bytes, MP4 headers may appear at byte offset 4-8) — it never reads the full file, making it efficient even for large uploads when used with streams. For processing thousands of uploads per minute, avoid loading entire files into memory: use fileTypeFromStream which reads just enough bytes to determine the type and then closes the stream without buffering the remainder. mmmagic's native binding adds per-call overhead from the Node.js addon boundary crossing — for high-throughput scenarios, this overhead accumulates noticeably compared to pure JavaScript alternatives.

MIME Type Standards and Edge Cases

MIME type detection has several genuinely tricky edge cases worth understanding. The TypeScript file extension maps to video/mp2t (MPEG-2 Transport Stream) because .ts was registered for that video format before TypeScript adopted the extension — mime-types returns this surprising result and you must handle it explicitly when serving TypeScript source files. SVG files are XML text and have no distinguishing magic bytes sequence — file-type cannot detect them from binary content, requiring a separate text-based check. Office documents (.docx, .xlsx, .pptx) are actually ZIP archives with specific internal structure — file-type correctly identifies them as application/zip by default since it reads magic bytes, not internal structure. To distinguish Office formats from plain ZIPs, you need to inspect the ZIP contents for the expected [Content_Types].xml file, which requires a ZIP parsing library.

Combining Both Libraries in Production

The most robust file handling implementations use both file-type and mime-types together, each for its appropriate purpose. Use file-type at the upload validation boundary to determine what a file actually is — this is the security-critical check that must inspect content. Use mime-types when serving files to clients, generating download links, or setting HTTP headers based on file extension — these are presentation-layer operations where you trust your own stored files and need only the MIME string for a known extension. A practical pattern: when you receive an upload, detect its true type with file-type, normalize the file extension to match (detected.ext), store both the file and its detected MIME type in your database, then use that stored MIME type (not a fresh detection) when serving the file. This separates the security-sensitive detection step from the routine serving step.

Cloud Storage Integration Patterns

File type detection typically happens at the upload handler before sending files to cloud storage (S3, R2, GCS). The detected MIME type should be set as the Content-Type metadata on the stored object, which ensures that when files are retrieved via presigned URLs or public access, the browser receives the correct content type and handles the file appropriately. Without this, uploaded images may be served with application/octet-stream content type, causing browsers to download them as binary blobs rather than rendering them. The file-type detection should occur before the multipart stream is fully buffered to avoid loading large files entirely into memory: read the first 4KB (sufficient for any magic byte detection), detect the type, reject disallowed types immediately without buffering the rest, and stream the remainder directly to cloud storage for allowed types. This pattern handles large file uploads efficiently without the memory overhead of buffering entire files.

Compare file processing and utility packages on PkgPulse →

See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.