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.

Install Bun first if you haven't: 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 freshclam
macOS
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}`);
Elysia validates the 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