Skip to main content

file-type vs mime-types vs mmmagic: File Detection in Node.js (2026)

·PkgPulse Team

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()

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on file-type v19.x, mime-types v2.x, and mmmagic v0.5.x.

Compare file processing and utility packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.