@axiomify/upload
v6.3.0
Published
RAM-safe multipart upload handler for Axiomify via Busboy — streaming, content-type validation, auto-cleanup on error.
Maintainers
Readme
@axiomify/upload
RAM-safe, stream-based multipart file upload for Axiomify. Files stream directly to disk via Busboy — no buffering in memory.
Install
npm install @axiomify/upload @axiomify/core busboy zodQuick start
import { Axiomify } from '@axiomify/core';
import { useUpload } from '@axiomify/upload';
import { z } from 'zod';
const app = new Axiomify();
// 1. Register the upload hook — once, before any routes
useUpload(app);
// 2. Declare file fields in route schema
app.route({
method: 'POST',
path: '/avatar',
schema: {
body: z.object({ userId: z.string() }), // text fields
files: {
avatar: {
autoSaveTo: './uploads/avatars',
accept: ['image/jpeg', 'image/png', 'image/webp'],
maxSize: 5 * 1024 * 1024, // 5 MB per file
},
},
},
handler: async (req, res) => {
const { userId } = req.body;
const file = req.files!.avatar;
// file.path — absolute path on disk
// file.originalName — original filename (sanitized)
// file.savedName — name on disk
// file.mimeType — detected MIME type
// file.size — bytes written
res.send({ userId, avatarPath: file.path });
},
});Options — useUpload(app, options?)
| Option | Default | Description |
| ------------------ | ------------- | ----------------------------------------------------------------------------- |
| dest | os.tmpdir() | Default save directory for files without autoSaveTo. |
| autoCleanup | false | Automatically delete all uploaded temporary files after the handler finishes. |
| limits.fileSize | 10 MiB | Global max file size in bytes. Per-field maxSize overrides this. |
| limits.files | 10 | Max number of files per request. |
| limits.fields | 50 | Max number of text fields per request. |
| limits.fieldSize | 1 MiB | Max text field value size in bytes. |
Per-field files schema
schema: {
files: {
// Field name in the multipart form
profilePhoto: {
autoSaveTo: './uploads/photos', // directory to save to
accept: ['image/jpeg', 'image/png'], // MIME type allowlist
maxSize: 2 * 1024 * 1024, // 2 MB (overrides global limit)
},
resume: {
autoSaveTo: './uploads/resumes',
accept: ['application/pdf'],
maxSize: 10 * 1024 * 1024, // 10 MB
},
},
}Security
- Path traversal: original filenames are sanitized —
../../../etc/passwdattempts are rejected with 400. - MIME type validation: files with disallowed MIME types are rejected. Checks the actual content-type from Busboy, not just the file extension.
- Size limits: enforced on the stream — clients cannot bypass the limit by omitting
Content-Length. - Automatic cleanup: if the handler throws, validation fails, or the client disconnects, any partially written files are automatically deleted via the
onErrorhook.
Multiple files, same field
schema: {
files: {
attachments: {
autoSaveTo: './uploads/attachments',
accept: ['application/pdf', 'image/jpeg'],
maxSize: 5 * 1024 * 1024,
},
},
},
handler: async (req, res) => {
// req.files.attachments is an array when multiple files share the same field name
const files = Array.isArray(req.files!.attachments)
? req.files!.attachments
: [req.files!.attachments];
res.send({ count: files.length, paths: files.map(f => f.path) });
},File Cleanup
If the request handler throws an error, validation fails, or the client disconnects before completion, any partially written files are automatically deleted via the @axiomify/upload onError hook.
For successful requests, you can clean up files automatically or manually:
Automatic Cleanup: Pass
autoCleanup: truewhen registeringuseUpload. This deletes all successfully uploaded temporary files after the route handler finishes execution:useUpload(app, { autoCleanup: true });Manual Cleanup: Call
req.cleanup()within your route handler after you have processed the files (e.g. after uploading them to S3 or copying them):handler: async (req, res) => { const file = req.files!.avatar; await uploadToS3(file.path); // Delete the local temporary file await req.cleanup?.(); res.send({ status: 'uploaded' }); };
Graceful shutdown
Files in progress when the server shuts down may be partially written. Call adapter.close() with a timeout to drain in-flight requests before exit:
process.on('SIGTERM', async () => {
await adapter.close();
process.exit(0);
});