Scanning file uploads in Next.js Pages Router (API Routes)

Next.js Pages Router API Routes live at pages/api/*.ts. Unlike App Router Route Handlers, they receive Node.js IncomingMessage / ServerResponse objects, so you can use Node.js-native tools like Multer directly — no need to convert a Web API File to a buffer first.

This guide is specifically for the Pages Router (pages/ directory). If you use the App Router (app/ directory), see Scanning file uploads in Next.js (App Router) instead.

Pages Router API Routes are fully supported and receive regular maintenance. There is no need to migrate to the App Router solely for file upload scanning.

Disable Next.js's built-in body parser

By default, Next.js parses the request body before your handler runs. Multer needs the raw stream, so you must disable the built-in parser by exporting a config object from the route file:

// pages/api/upload.ts
export const config = {
  api: {
    bodyParser: false,
  },
};

Without this, Multer will not receive any data and the request will appear empty.

Install

npm install pompelmi multer
npm install --save-dev @types/multer

Multer helper for Next.js

Multer uses Express-style middleware callbacks. Next.js API Routes are plain Node.js handlers, not Express — so you need a small wrapper that promisifies the middleware:

// lib/multer.ts
import multer from 'multer';
import type { NextApiRequest, NextApiResponse } from 'next';
import os from 'os';

const upload = multer({
  dest:   os.tmpdir(),
  limits: { fileSize: 50 * 1024 * 1024 },
});

type MiddlewareFn = (
  req: NextApiRequest,
  res: NextApiResponse,
  next: (err?: unknown) => void
) => void;

export function runMiddleware(
  req: NextApiRequest,
  res: NextApiResponse,
  fn: MiddlewareFn
): Promise<void> {
  return new Promise((resolve, reject) => {
    fn(req, res, (err?: unknown) => {
      if (err) reject(err);
      else resolve();
    });
  });
}

export const uploadSingle = upload.single('file') as unknown as MiddlewareFn;

Complete API Route

// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { scan, Verdict } from 'pompelmi';
import fs from 'fs';
import { runMiddleware, uploadSingle } from '@/lib/multer';

export const config = {
  api: { bodyParser: false },
};

interface MulterRequest extends NextApiRequest {
  file?: Express.Multer.File;
}

export default async function handler(
  req: MulterRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed.' });
  }

  try {
    // Run Multer — writes the file to /tmp
    await runMiddleware(req, res, uploadSingle);
  } catch (err) {
    return res.status(400).json({ error: 'File upload failed.' });
  }

  if (!req.file) {
    return res.status(400).json({ error: 'No file provided.' });
  }

  const tmpPath = req.file.path;

  try {
    const verdict = await scan(tmpPath);

    if (verdict === Verdict.Malicious) {
      return res.status(400).json({ error: 'Malware detected. Upload rejected.' });
    }

    if (verdict === Verdict.ScanError) {
      return res.status(422).json({
        error: 'Scan could not complete. Upload rejected as a precaution.',
      });
    }

    // File is clean — move to permanent storage
    return res.status(200).json({ status: 'ok', name: req.file.originalname });

  } catch (err) {
    const message = err instanceof Error ? err.message : 'Scan failed';
    return res.status(500).json({ error: message });

  } finally {
    if (fs.existsSync(tmpPath)) {
      fs.unlinkSync(tmpPath);
    }
  }
}
Test with the EICAR test string to confirm your pipeline is working. A POST to /api/upload with a file containing the EICAR string should return HTTP 400 with "Malware detected".

TypeScript types for pompelmi

pompelmi does not ship type declarations. Add this declaration file to your project:

types/pompelmi.d.ts
declare module 'pompelmi' {
  interface ScanOptions {
    host?:    string;
    port?:    number;
    timeout?: number;
  }

  const Verdict: Readonly<{
    Clean:     unique symbol;
    Malicious: unique symbol;
    ScanError: unique symbol;
  }>;

  function scan(filePath: string, options?: ScanOptions): Promise<symbol>;
  export { scan, Verdict };
}

Add the types directory to your tsconfig.json typeRoots or ensure it is included in the project files.

Next steps