Works with any backend npm install Zero dependencies

Drop-in upload server via multipleupload/server

A Connect-style middleware that closes the "we don't have a Node server" gap. Mounts POST /upload (single), POST /chunk (chunked), and GET /chunk/status (resume) - the default endpoints the standalone JS client expects. Files land on local disk; the onComplete callback is your hook to stream the bytes to S3, push to a queue, or persist a database row. Mirrors what ajaxupload.axd does for ASP.NET WebForms - but for Node.

Subpath import: multipleupload/server ships in the same npm package as the client. Peer dependency: busboy (the Node ecosystem standard for multipart parsing - same library Express uses internally).
Install
npm install multipleupload express busboy
Minimal Express server
const express = require('express');
const multipleupload = require('multipleupload/server');

const app = express();

app.use(multipleupload({
 uploadDir: './uploads',
 maxFileSize: 5 * 1024 * 1024 * 1024, // 5 GB
 allowedExtensions: ['.jpg', '.png', '.pdf'], // omit to allow all
 onComplete: (info) => {
 console.log('saved', info.filePath, info.fileSize, 'bytes');
 // Persist info.{fileGuid, fileName, fileSize, filePath} to your DB,
 // upload to S3 + delete the local copy, push to a queue, etc.
 }
}));

app.listen(3000, () => console.log('upload server on :3000'));
Mount under a prefix
// Serve under /api/upload - strips the prefix before matching /upload, /chunk.
app.use('/api/upload', multipleupload({ basePath: '/api/upload', uploadDir: './uploads' }));

// Client side: just point the uploader at the prefix.
new MultipleUpload('#uploader', {
 uploadUrl: '/api/upload/upload',
 chunkUrl: '/api/upload/chunk',
 chunked: true,
 chunkSize: 5 * 1024 * 1024
});
Wire protocol
  • POST /upload - multipart form-data with field file. Returns { success, fileGuid, fileName, fileSize, filePath }.
  • POST /chunk - fields id, chunkIndex, totalChunks, fileName, fileSize, plus a file part. Returns { success, complete: false, received } while assembling, then { success, complete: true, fileGuid, fileName, ... } on the last chunk.
  • GET /chunk/status?id=<taskId> - returns { success, received: [0,1,2,...] }. The client uses this to resume from the highest received index.
Stream to S3 / R2 in onComplete
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const fs = require('node:fs');
const s3 = new S3Client({ region: 'us-east-1' });

app.use(multipleupload({
 uploadDir: './uploads',
 onComplete: async (info) => {
 await s3.send(new PutObjectCommand({
 Bucket: 'my-uploads',
 Key: info.fileGuid + '/' + info.fileName,
 Body: fs.createReadStream(info.filePath)
 }));
 fs.unlinkSync(info.filePath); // local copy was just a staging area
 console.log('archived to S3:', info.fileGuid);
 }
}));
Fastify adaptation

Fastify's request / reply objects are Express-compatible enough - wrap with fastify.use() or @fastify/express:

const fastify = require('fastify')();
await fastify.register(require('@fastify/express'));
fastify.use(multipleupload({ uploadDir: './uploads' }));
await fastify.listen({ port: 3000 });
What this is and isn't
  • Is: a small, dependency-light handler covering single + chunked uploads with resume, validation hooks, configurable disk layout.
  • Isn't: a tus 1.0 server (use tusd for that - the client speaks tus already), an S3 signer (sign with the AWS SDK directly), or an OAuth broker for cloud sources (those land in a future multipleupload/companion).
  • For the direct-to-S3 / Azure / tus strategies, this middleware isn't on the upload path - only the signing endpoints need to live in your server.