file-type vs mime-types vs mmmagic: File Detection in Node.js (2026)
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-typedetects 100+ formats: images, audio, video, archives, documents, executablesmime-typesis 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
| Feature | file-type | mime-types | mmmagic |
|---|---|---|---|
| Detection method | Magic bytes (secure) | Extension lookup | libmagic (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.