Well great… I’m a MASSIVE Next.js fan… I’ve been using it exclusively for all my recent projects. Since you’re self-hosted… what if you were to leverage a filesystem store for your pdf’s…

{
  "_id": ObjectId("..."),
  "title": "title of your PDF",
  "filePath": "/uploads/your.pdf",
  "mimeType": "application/pdf",
  "uploadedAt": ISODate("2025-04-01T10:00:00Z")
}

I’m using Multer for file uploads.

I’d build a project structure something like this:

your_app/
├── pages/
│   └── api/
│       ├── upload.js
│       └── file/[id].js
├── uploads/
├── lib/
│   └── mongodb.js

Basic MongoDB Connection:

import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI;
const client = new MongoClient(uri);

export async function connectToDatabase() {
  if (!client.isConnected()) await client.connect();
  const db = client.db(); // or specify your specific db name
  return { db, client };
}

Then create an api upload:

import nextConnect from 'next-connect';
import multer from 'multer';
import { connectToDatabase } from '../../lib/mongodb';
import path from 'path';
import fs from 'fs';

const uploadDir = './uploads';
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

const upload = multer({ dest: uploadDir });

const apiRoute = nextConnect({
  onError(error, req, res) {
    res.status(501).json({ error: `Upload failed: ${error.message}` });
  },
  onNoMatch(req, res) {
    res.status(405).json({ error: `Method ${req.method} not allowed` });
  },
});

apiRoute.use(upload.single('file'));

apiRoute.post(async (req, res) => {
  const { db } = await connectToDatabase();
  const { originalname, filename, mimetype, path: filepath } = req.file;

  const result = await db.collection('files').insertOne({
    originalname,
    filename,
    mimetype,
    filepath,
    uploadedAt: new Date()
  });

  res.status(200).json({ success: true, id: result.insertedId });
});

export default apiRoute;
export const config = { api: { bodyParser: false } };

and maybe a file server:

import { connectToDatabase } from '../../../lib/mongodb';
import { ObjectId } from 'mongodb';
import fs from 'fs';
import path from 'path';

export default async function handler(req, res) {
  const {
    query: { id },
  } = req;

  const { db } = await connectToDatabase();
  const fileDoc = await db.collection('files').findOne({ _id: new ObjectId(id) });

  if (!fileDoc) {
    res.status(404).json({ error: 'File not found' });
    return;
  }

  const filePath = path.join(process.cwd(), fileDoc.filepath);
  const fileStream = fs.createReadStream(filePath);

  res.setHeader('Content-Type', fileDoc.mimetype);
  fileStream.pipe(res);
}

You could use a simple form in your UI:

const uploadFile = async (event) => {
  const formData = new FormData();
  formData.append("file", event.target.files[0]);

  const res = await fetch("/api/upload", {
    method: "POST",
    body: formData,
  });

  const data = await res.json();
  console.log("Uploaded File ID:", data.id);
};

Hope this helps! Let us know how you make out.

Regards,
Mike
https://mlynn.org