Two years ago, I shipped my first production app with file uploads. A week later, my mentor asked: "Are you scanning those files for malware?"
I wasn't.
The Google Rabbit Hole
Like any developer, I Googled "node js antivirus file upload." Here's what I found:
- ClamAV — the open-source standard. Great! Let's use it.
- clamscan npm package — 47 configuration options.
-
clamav.js — requires running
clamddaemon. - Various tutorials — "First, configure your socket connection..."
I just wanted to scan a file. Why did I need to understand Unix sockets?
My First Attempt
After an hour of reading docs, I had something like this:
const NodeClam = require('clamscan');
const clamscan = new NodeClam().init({
removeInfected: false,
quarantineInfected: false,
scanLog: null,
debugMode: false,
fileList: null,
scanRecursively: true,
clamscan: {
path: '/usr/bin/clamscan',
db: null,
scanArchives: true,
active: true
},
clamdscan: {
socket: '/var/run/clamav/clamd.ctl',
host: false,
port: false,
timeout: 60000,
localFallback: true,
path: '/usr/bin/clamdscan',
configFile: null,
multiscan: true,
reloadDb: false,
active: true,
bypassTest: false,
},
preference: 'clamdscan'
});
// ... 30 more lines to actually scan a file
It didn't work. The socket path was wrong on my Mac. I spent another hour debugging.
The Daemon Problem
Most ClamAV wrappers assume you're running clamd — a background daemon that keeps virus definitions in memory for faster scanning.
This makes sense for high-throughput enterprise systems. But I was building a side project. I didn't want to manage a daemon. I didn't want to configure systemd. I didn't want to think about socket permissions.
I just wanted to answer one question: is this file safe?
What I Actually Needed
After three hours of frustration, I wrote down what I actually wanted:
const result = await scanFile('/path/to/upload.zip');
// "Clean" or "Malicious" — that's it
No daemon. No configuration object. No socket paths. Just a function that takes a file and returns an answer.
So I built it.
pompelmi: ClamAV for Humans
const pompelmi = require('pompelmi');
const result = await pompelmi.scan('/path/to/file.zip');
// "Clean" | "Malicious" | "ScanError"
That's the entire API. One function. Three possible results.
Here's the same Express middleware, before and after:
Before (47 lines)
const NodeClam = require('clamscan');
const multer = require('multer');
// ... 35 lines of configuration ...
app.post('/upload', upload.single('file'), async (req, res) => {
try {
const clam = await clamscan;
const {isInfected, viruses} = await clam.isInfected(req.file.path);
if (isInfected) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: 'Malware detected', viruses });
}
res.json({ success: true });
} catch (err) {
// Is this a scan error or a config error? Who knows!
res.status(500).json({ error: 'Scan failed' });
}
});
After (12 lines)
const pompelmi = require('pompelmi');
const multer = require('multer');
app.post('/upload', upload.single('file'), async (req, res) => {
const result = await pompelmi.scan(req.file.path);
if (result === 'Malicious') {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: 'Malware detected' });
}
if (result === 'ScanError') {
// Scan couldn't complete — treat as untrusted
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: 'File rejected' });
}
res.json({ success: true });
});
How It Works (The Boring Way)
pompelmi doesn't do anything clever:
- Check that the file exists
- Spawn
clamscan --no-summary <path> - Read the exit code
- Map it to a string
No stdout parsing. No regex. No daemon. No socket. Just a child process and an exit code.
Exit 0 → "Clean"
Exit 1 → "Malicious"
Exit 2 → "ScanError"
ClamAV has been doing this reliably for 20 years. I just wrapped it in a function.
"But What About Performance?"
Yes, spawning clamscan for every file is slower than using the daemon. Each scan loads the virus database from disk (~300MB).
For a side project handling 100 uploads/day? Doesn't matter.
For a startup processing 10,000 files/hour? You probably have a dedicated security engineer who knows how to configure clamd.
pompelmi is for the 99% of developers who just need something — and "slow but working" beats "fast but misconfigured."
The Point
Developer tools should meet you where you are.
When I was a junior developer, I didn't need 47 configuration options. I needed one function that worked. I needed to ship something on Friday and not think about Unix sockets.
If you're building something bigger, you'll outgrow pompelmi. That's fine. By then, you'll understand why you need the daemon, and configuring it won't feel like dark magic.
But if you're building your first app with file uploads, and you just want to scan for malware without a 3-hour detour — npm install pompelmi.
npm install pompelmi
pompelmi is Italian for "grapefruits." I named it that because I was eating one when I finally got the code working at 2am. Sometimes that's all the reason you need.
Top comments (2)
Have you given clam in docker a chance already? Tbh, this is something I haven't thought about yet... 😁
That's a great suggestion! I'm currently working on it, and you'll definitely see it included in the 1.1.x release. 😁