October 14, 2026
How to Route Clean and Suspicious File Uploads in Node.js
Not every file our upload endpoint accepts deserves the same treatment.

By Cloudmersive
2 min read
For example, a clean PDF should flow straight into our normal handling, and a file that trips a virus & malware scanner shouldn't be dropped on the floor (it definitely shouldn't continue downstream either). What we want is a fork in the road: clean files go one way, suspicious files go another.
In this article, we'll build that fork in Node.js with Express and the Cloudmersive Virus Scan API, sending each upload down the path that fits it based on scan results.
Installing the packages
Before we get into the bulk of our code, let's install the Virus Scan API client:
npm install express multer cloudmersive-virus-api-client --savenpm install express multer cloudmersive-virus-api-client --saveWriting up the scan client
The client reads its key from the shared instance; we'll pull ours from an environment variable:
const CloudmersiveVirusApiClient = require('cloudmersive-virus-api-client');
const client = CloudmersiveVirusApiClient.ApiClient.instance;
client.authentications['Apikey'].apiKey = process.env.CLOUDMERSIVE_API_KEY;
const scanApi = new CloudmersiveVirusApiClient.ScanApi();const CloudmersiveVirusApiClient = require('cloudmersive-virus-api-client');
const client = CloudmersiveVirusApiClient.ApiClient.instance;
client.authentications['Apikey'].apiKey = process.env.CLOUDMERSIVE_API_KEY;
const scanApi = new CloudmersiveVirusApiClient.ScanApi();Scanning a file
The scan method we'll be using is callback-based, so we'll wrap it in a promise to keep the rest of our code flat and await-friendly. The Advanced Scan metho lets us flag the threat categories our endpoint should (probably) never accept: executables, scripts, macros, and more.
function scanBuffer(buffer) {
const options = {
allowExecutables: false,
allowScripts: false,
allowMacros: false,
allowInvalidFiles: false,
allowPasswordProtectedFiles: false,
allowUnsafeArchives: false,
};
return new Promise((resolve, reject) => {
scanApi.scanFileAdvanced(buffer, options, (error, data) => {
if (error) return reject(error);
resolve(data);
});
});
}function scanBuffer(buffer) {
const options = {
allowExecutables: false,
allowScripts: false,
allowMacros: false,
allowInvalidFiles: false,
allowPasswordProtectedFiles: false,
allowUnsafeArchives: false,
};
return new Promise((resolve, reject) => {
scanApi.scanFileAdvanced(buffer, options, (error, data) => {
if (error) return reject(error);
resolve(data);
});
});
}Forking clean from suspicious
When we have a scan result in hand, routing is just one branch. Clean files go to normal handling; flagged files are diverted, and we hold onto the scan details so a reviewer can see what triggered the diversion:
async function routeUpload(file) {
const result = await scanBuffer(file.buffer);
if (result.cleanResult === true) {
return handleClean(file);
}
return handleSuspicious(file, result);
}
function handleClean(file) {
// forward to your normal pipeline (storage, queue, etc.)
return { status: 'accepted', file: file.originalname };
}
function handleSuspicious(file, result) {
// divert to quarantine, keeping the reasons for review
return {
status: 'quarantined',
file: file.originalname,
reasons: result.foundViruses || [],
};
}async function routeUpload(file) {
const result = await scanBuffer(file.buffer);
if (result.cleanResult === true) {
return handleClean(file);
}
return handleSuspicious(file, result);
}
function handleClean(file) {
// forward to your normal pipeline (storage, queue, etc.)
return { status: 'accepted', file: file.originalname };
}
function handleSuspicious(file, result) {
// divert to quarantine, keeping the reasons for review
return {
status: 'quarantined',
file: file.originalname,
reasons: result.foundViruses || [],
};
}The upload route
Finally, an Express route ties it all together. Multer's memory storage hands us the file as a buffer, which is exactly what the scanner wants:
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload', upload.single('file'), async (req, res) => {
try {
const outcome = await routeUpload(req.file);
res.json(outcome);
} catch (err) {
res.status(500).json({ error: 'Scan failed.' });
}
});
app.listen(3000);const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload', upload.single('file'), async (req, res) => {
try {
const outcome = await routeUpload(req.file);
res.json(outcome);
} catch (err) {
res.status(500).json({ error: 'Scan failed.' });
}
});
app.listen(3000);Now, clean uploads come back accepted. Anything suspicious comes back quarantined with the specific reasons attached.
Conclusion
The careful routing we've implemented beats a flat accept-or-reject workflow. Each outcome gets handling that suits it, and nothing suspicious slips through to the next stage. The scan is ultimately what decides which path a file takes; what each path does, however, is entirely yours to define.