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 fieldfile. Returns{ success, fileGuid, fileName, fileSize, filePath }.POST /chunk- fieldsid,chunkIndex,totalChunks,fileName,fileSize, plus afilepart. 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
tusdfor 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 futuremultipleupload/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.