Virus scanning in Elysia.js with pompelmi
Elysia is a TypeScript-first web framework built specifically for
Bun. It is known for its end-to-end type safety, ergonomic
plugin system, and performance. File uploads in Elysia use Bun's Web API
File type, validated at the route level with
t.File() from Elysia's schema builder.
pompelmi calls clamscan or connects to clamd — both of which
are available in Bun via its Node.js compatibility layer. No extra
configuration is required.
curl -fsSL https://bun.sh/install | bash
Install
bun add elysia pompelmi
Install ClamAV on the host system:
Linux (Debian / Ubuntu)sudo apt-get install -y clamav && sudo freshclammacOS
brew install clamav && freshclam
Upload route
Declare the file field with t.File() in the body schema.
Elysia validates the request body and provides the file as a Web API
File object. Write it to a temp path, scan, then clean up:
// src/index.ts
import { Elysia, t } from 'elysia';
import { scan, Verdict } from 'pompelmi';
import { writeFile, unlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomBytes } from 'node:crypto';
const app = new Elysia()
.post(
'/upload',
async ({ body, set }) => {
const file = body.file;
let tmpPath: string | null = null;
try {
const MAX_BYTES = 50 * 1024 * 1024;
if (file.size > MAX_BYTES) {
set.status = 413;
return { error: 'File too large.' };
}
const buffer = Buffer.from(await file.arrayBuffer());
const ext = file.name.split('.').pop()?.toLowerCase() ?? 'bin';
tmpPath = join(tmpdir(), randomBytes(16).toString('hex') + '.' + ext);
await writeFile(tmpPath, buffer);
const verdict = await scan(tmpPath);
if (verdict === Verdict.Malicious) {
set.status = 400;
return { error: 'Malware detected. Upload rejected.' };
}
if (verdict === Verdict.ScanError) {
set.status = 422;
return { error: 'Scan could not complete. Upload rejected.' };
}
// File is clean — persist to storage here
return { status: 'ok', name: file.name };
} catch (err) {
set.status = 500;
return { error: err instanceof Error ? err.message : 'Unknown error' };
} finally {
if (tmpPath && existsSync(tmpPath)) {
await unlink(tmpPath).catch(() => {});
}
}
},
{
body: t.Object({
file: t.File(),
}),
}
)
.listen(3000);
console.log(`Listening on http://localhost:${app.server?.port}`);
t.File() constraint before your handler
runs. If the request contains no file field, Elysia returns a 422 error
automatically — you do not need to check for a missing file yourself.
Reusable scan plugin
Elysia's plugin system lets you encapsulate the scan logic and share it
across multiple routes. The plugin adds a scanFile helper to
the app's context:
// src/plugins/antivirus.ts
import { Elysia } from 'elysia';
import { scan, Verdict } from 'pompelmi';
import { writeFile, unlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomBytes } from 'node:crypto';
export const antivirusPlugin = new Elysia({ name: 'antivirus' })
.derive(() => ({
async scanFile(file: File): Promise<'clean' | 'malicious' | 'error'> {
const buffer = Buffer.from(await file.arrayBuffer());
const ext = file.name.split('.').pop()?.toLowerCase() ?? 'bin';
const tmpPath = join(tmpdir(), randomBytes(16).toString('hex') + '.' + ext);
try {
await writeFile(tmpPath, buffer);
const verdict = await scan(tmpPath);
if (verdict === Verdict.Clean) return 'clean';
if (verdict === Verdict.Malicious) return 'malicious';
return 'error';
} finally {
if (existsSync(tmpPath)) await unlink(tmpPath).catch(() => {});
}
},
}));
Use the plugin in any route:
// src/index.ts
import { Elysia, t } from 'elysia';
import { antivirusPlugin } from './plugins/antivirus';
const app = new Elysia()
.use(antivirusPlugin)
.post(
'/upload',
async ({ body, scanFile, set }) => {
const result = await scanFile(body.file);
if (result === 'malicious') { set.status = 400; return { error: 'Malware detected.' }; }
if (result === 'error') { set.status = 422; return { error: 'Scan incomplete.' }; }
return { status: 'ok', name: body.file.name };
},
{ body: t.Object({ file: t.File() }) }
)
.listen(3000);
Next steps
- Using Hono instead of Elysia? See Virus scanning in Hono.js with pompelmi.
- Uploading to object storage after scanning? See Scanning files before uploading to Cloudflare R2 — a natural pairing for Bun-based services.
- Full upload security checklist? Read Node.js file upload security checklist.