Use Pompelmi with Fastify
This guide shows a minimal integration with Fastify v5. You’ll register the @pompelmi/fastify-plugin preHandler and expose a POST /scan route that validates files (extension, MIME, size), runs an optional scanner, and returns a clear clean / suspicious / malicious verdict.
Works with Node 18+ and requires
@fastify/multipartv9+.
1) Install
Section titled “1) Install”pnpm add fastify @fastify/multipart @pompelmi/fastify-plugin2) Environment
Section titled “2) Environment”Create .env (or export the variable in your shell):
PORT=42003) Minimal server
Section titled “3) Minimal server”Create server.ts (or server.js):
import Fastify from 'fastify';import multipart from '@fastify/multipart';import { createUploadGuard } from '@pompelmi/fastify-plugin';
const app = Fastify({ logger: true });
// Register multipart support (required)await app.register(multipart);
// Create the upload guard preHandlerconst uploadGuard = createUploadGuard({ allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'], maxFileSizeBytes: 50 * 1024 * 1024, // 50 MB stopOn: 'suspicious', // block suspicious and above failClosed: true, // reject on scan errors});
// POST /scan — protected by the upload guardapp.post('/scan', { preHandler: uploadGuard }, async (request, reply) => { const { verdict, results } = (request as any).pompelmi;
if (verdict === 'malicious' || verdict === 'suspicious') { return reply.code(422).send({ result: { malicious: true }, verdict, reasons: results.map((r: any) => r.reason).filter(Boolean), }); }
return reply.send({ result: { malicious: false }, verdict });});
const port = Number(process.env.PORT || 4200);await app.listen({ port, host: '0.0.0.0' });console.log(`pompelmi fastify listening on http://localhost:${port}`);Notes
createUploadGuardreturns a FastifypreHandlerthat reads all multipart file parts and attachesrequest.pompelmi = { files, results, verdict }for use in the route handler.- Rejected uploads (bad extension, bad MIME, too large, or malicious verdict) send a
422response and stop processing before the route handler is called. - If your engine requires headers/auth, bring your own scanner function (see below).
4) Add a custom scanner (optional)
Section titled “4) Add a custom scanner (optional)”Pass any async function with signature (bytes: Uint8Array, meta: FileMeta) => Promise<ScanResult>:
import { createUploadGuard, type FileMeta, type ScanResult } from '@pompelmi/fastify-plugin';
async function myScanner(bytes: Uint8Array, meta: FileMeta): Promise<ScanResult> { // Forward to an external engine, run YARA locally, etc. const form = new FormData(); form.append('file', new Blob([bytes], { type: meta.mimetype }), meta.originalname); const res = await fetch(`${process.env.POMPELMI_ENGINE_URL}/scan`, { method: 'POST', body: form, }); const data = await res.json(); return { severity: data?.result?.malicious ? 'malicious' : 'clean', reason: data?.result?.reason, };}
const uploadGuard = createUploadGuard({ allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'], maxFileSizeBytes: 50 * 1024 * 1024, scanner: myScanner,});5) Wire up the UI (client)
Section titled “5) Wire up the UI (client)”Point the UI components to your Fastify route:
# Next.js (client)NEXT_PUBLIC_POMPELMI_URL=http://localhost:4200import { UploadButton } from '@pompelmi/ui-react';
<UploadButton action={`${process.env.NEXT_PUBLIC_POMPELMI_URL?.replace(/\/$/, '')}/scan`} />6) Test the flow
Section titled “6) Test the flow”- Upload a clean JPG → expect
{ result: { malicious: false }, verdict: "clean" } - Use the official EICAR test file → expect
{ result: { malicious: true }, verdict: "malicious" }(requires a scanner) - Watch the Fastify logger output for request details
7) Production hardening (checklist)
Section titled “7) Production hardening (checklist)”- Tighten
allowedMimeTypesand addincludeExtensionsto restrict file types further. - Add auth middleware (e.g., JWT) before the upload guard.
- Set a reverse proxy (Nginx/Cloudflare) with body size limits and rate limits.
- Use
onScanEventcallback for telemetry/logging of scan verdicts. - Make the engine URL configurable via secret manager/ENV.
API reference
Section titled “API reference”createUploadGuard(options)
Section titled “createUploadGuard(options)”| Option | Type | Default | Description |
|---|---|---|---|
allowedMimeTypes | string[] | [] (allow all) | MIME types to accept; others get 422 |
includeExtensions | string[] | [] (allow all) | File extensions (without dot) to accept |
maxFileSizeBytes | number | MAX_SAFE_INTEGER | Max bytes per file; larger files get 422 |
stopOn | "suspicious" | "malicious" | "suspicious" | Minimum verdict level that triggers rejection |
failClosed | boolean | true | If true, scan errors are treated as malicious |
scanner | ScannerFn | undefined | Custom scanner function or { scan } object |
onScanEvent | (ev: unknown) => void | undefined | Callback for scan telemetry |
request.pompelmi (injected by the preHandler)
Section titled “request.pompelmi (injected by the preHandler)”{ files: string[]; // original filenames processed results: ScanResult[]; // per-file scan results verdict: Severity; // overall verdict: "clean" | "suspicious" | "malicious"}Troubleshooting
Section titled “Troubleshooting”- 422 extension_not_allowed → Add the extension to
includeExtensionsor remove the guard. - 422 mime_not_allowed → Add the MIME type to
allowedMimeTypesor remove the guard. - 422 file_too_large → Increase
maxFileSizeBytesor reject large files on the client. - UI shows only ERROR → Open DevTools → Network, inspect the
/scanresponse JSON from your server.