fastify-multipart-file
v1.0.2
Published
Fastify plugin for handling multipart/form-data with file validation, type coercion, and nested object support
Maintainers
Readme
fastify-multipart-file
A powerful Fastify plugin for handling multipart/form-data requests with automatic type coercion, file validation, and nested object support.
Features
- Automatic Type Coercion: Converts string values to appropriate types (number, boolean, object, array) based on JSON schema
- File Validation: Validates file size and MIME type based on schema definitions
- Nested Object Support: Handles complex nested structures using array notation (e.g.,
items[0].name) - File Processing: Automatically processes and validates file uploads
- Schema-Driven: Uses Fastify's JSON schema validation for type inference
- TypeScript Support: Full TypeScript support with type definitions
Installation
npm install fastify-multipart-fileNote: This package includes @fastify/multipart as a dependency and registers it automatically. No additional setup needed!
Usage
Basic Setup
import Fastify from "fastify";
import { register as multipartHandler } from "fastify-multipart-file";
const fastify = Fastify();
// Register the multipart handler (includes @fastify/multipart automatically)
await fastify.register(multipartHandler);
await fastify.listen({ port: 3000 });Example Route with File Upload
import S from "fluent-json-schema";
fastify.post("/upload", {
schema: {
body: S.object()
.prop("name", S.string().required())
.prop("age", S.number())
.prop("isActive", S.boolean())
.prop(
"avatar",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024) // 5MB max
.raw({ accept: ["image/jpeg", "image/png", "image/gif"] })
),
},
handler: async (request, reply) => {
const { name, age, isActive, avatar } = request.body;
// 'name' is string
// 'age' is number (auto-converted)
// 'isActive' is boolean (auto-converted)
// 'avatar' is File object with buffer
console.log(avatar.buffer); // Buffer
console.log(avatar.mimetype); // e.g., 'image/jpeg'
console.log(avatar.size); // File size in bytes
console.log(avatar.originalName); // Original filename
return { success: true };
},
});Nested Objects and Arrays
fastify.post("/nested", {
schema: {
body: S.object()
.prop("items[0].name", S.string())
.prop("items[0].quantity", S.number())
.prop("items[1].name", S.string())
.prop("items[1].quantity", S.number())
.prop("metadata.tags", S.array()),
},
handler: async (request, reply) => {
const { items, metadata } = request.body;
// items is: [
// { name: '...', quantity: 123 },
// { name: '...', quantity: 456 }
// ]
// metadata is: { tags: [...] }
return { success: true };
},
});Multiple File Uploads
The plugin supports multiple file uploads in the same request using different field names or array notation.
Option 1: Different Field Names
fastify.post("/upload-multiple", {
schema: {
body: S.object()
.prop("name", S.string().required())
.prop(
"avatar",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024) // 5MB max
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"document",
S.string()
.format("binary")
.maxLength(10 * 1024 * 1024) // 10MB max
.raw({ accept: ["application/pdf"] })
)
.prop(
"thumbnail",
S.string()
.format("binary")
.maxLength(1 * 1024 * 1024) // 1MB max
.raw({ accept: ["image/png", "image/gif"] })
),
},
handler: async (request, reply) => {
const { name, avatar, document, thumbnail } = request.body;
// Process multiple files
console.log("Avatar:", avatar.originalName, avatar.size);
console.log("Document:", document.originalName, document.size);
console.log("Thumbnail:", thumbnail.originalName, thumbnail.size);
// Save files to storage
await saveFile(avatar.buffer, avatar.name);
await saveFile(document.buffer, document.name);
await saveFile(thumbnail.buffer, thumbnail.name);
return {
success: true,
files: {
avatar: avatar.name,
document: document.name,
thumbnail: thumbnail.name,
},
};
},
});Option 2: Array of Files
fastify.post("/upload-array", {
schema: {
body: S.object()
.prop("title", S.string().required())
.prop(
"images[0]",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"images[1]",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"images[2]",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
),
},
handler: async (request, reply) => {
const { title, images } = request.body;
// images is an array of File objects
console.log(`Received ${images.length} images for: ${title}`);
const uploadedFiles = [];
for (const [index, image] of images.entries()) {
console.log(`Image ${index}:`, image.originalName, image.size);
await saveFile(image.buffer, image.name);
uploadedFiles.push(image.name);
}
return {
success: true,
title,
filesCount: images.length,
files: uploadedFiles,
};
},
});Option 3: Mixed Files and Data
fastify.post("/product", {
schema: {
body: S.object()
.prop("name", S.string().required())
.prop("price", S.number().required())
.prop("description", S.string())
.prop("inStock", S.boolean())
.prop(
"mainImage",
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"gallery[0]",
S.string()
.format("binary")
.maxLength(3 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"gallery[1]",
S.string()
.format("binary")
.maxLength(3 * 1024 * 1024)
.raw({ accept: ["image/jpeg", "image/png"] })
)
.prop(
"manual",
S.string()
.format("binary")
.maxLength(20 * 1024 * 1024)
.raw({ accept: ["application/pdf"] })
),
},
handler: async (request, reply) => {
const { name, price, description, inStock, mainImage, gallery, manual } =
request.body;
// Save product data
const product = {
name, // string
price, // number (auto-converted)
description, // string
inStock, // boolean (auto-converted)
mainImageUrl: await uploadToS3(mainImage.buffer, mainImage.name),
galleryUrls: await Promise.all(
gallery.map((img) => uploadToS3(img.buffer, img.name))
),
manualUrl: await uploadToS3(manual.buffer, manual.name),
};
return { success: true, product };
},
});How It Works
The plugin adds two Fastify hooks:
preValidation Hook: Processes multipart fields before validation
- Detects file uploads and validates them
- Converts string values to appropriate types based on schema
- Handles nested object notation
preHandler Hook: Reconstructs file buffers after validation
- Restores file objects with proper Buffer instances
- Merges file data back into the request body
Schema Properties
File Upload Schema
S.string()
.format("binary")
.maxLength(5 * 1024 * 1024) // Maximum file size in bytes
.raw({ accept: ["image/jpeg", "image/png"] }); // Allowed MIME typesType Coercion Schema
The plugin automatically converts values based on the schema type:
S.number()→ Converts to numberS.integer()→ Converts to integerS.boolean()→ Converts to boolean ('true', '1' → true)S.object()→ Parses JSON string to objectS.array()→ Parses JSON string to array
If no schema is provided, the plugin attempts to infer the type from the value.
API
register(fastify: FastifyInstance): Promise<void>
Main plugin registration function.
Exported Types
import {
// Types
FileUpload,
MultipartField,
SchemaProperty,
SchemaBody,
SerializedFile,
ProcessedFile,
ValidationError,
// Classes
File,
FileMapper,
UnprocessedEntityError,
// Helpers
JsonHelper,
UuidHelper,
// Type Guards
isValidationError,
} from "fastify-multipart-file";File Class
class File {
name?: string; // Generated unique filename with extension
mimetype?: string; // MIME type (e.g., 'image/jpeg')
encoding?: string; // Encoding (e.g., '7bit')
buffer: Buffer; // File content as Buffer
size: number; // File size in bytes
originalName?: string; // Original filename from upload
}FileMapper
class FileMapper {
static from(uploadFile: FileUpload): File;
}Converts a raw file upload to a processed File object with a unique name.
Error Handling
The plugin throws UnprocessedEntityError (HTTP 422) when:
- File size exceeds
maxLength - File MIME type is not in the
acceptlist
The error follows a standard validation error format:
import { isValidationError, ValidationError } from "fastify-multipart-file";
try {
// Handle multipart request
} catch (error) {
if (isValidationError(error)) {
console.log(error.statusCode); // 422
console.log(error.message); // 'Validation error'
console.log(error.validation); // Array of { field, message }
// Example output:
// [
// {
// field: 'avatar',
// message: 'File size exceeds the maximum allowed size of 5242880 bytes.'
// }
// ]
}
}Error Response Format:
{
"statusCode": 422,
"message": "Validation error",
"validation": [
{
"field": "avatar",
"message": "File size exceeds the maximum allowed size of 5242880 bytes."
}
]
}Advanced Usage
Custom File Processing
import { FileMapper, File } from 'fastify-multipart-file';
fastify.post('/custom', async (request, reply) {
const { document } = request.body as { document: File };
// Access file properties
console.log(document.name); // UUID-based filename
console.log(document.originalName); // Original filename
console.log(document.buffer); // Buffer for processing
console.log(document.size); // Size in bytes
console.log(document.mimetype); // MIME type
// Save to disk, upload to S3, etc.
await saveToS3(document.buffer, document.name);
return { fileId: document.name };
});Validation Helpers
import {
isValidFileField,
validateFileSize,
validateFileMimeType,
} from "fastify-multipart-file";
// Manually validate files if needed
const file = {
/* File object */
};
validateFileSize(file, 1024 * 1024, "avatar"); // Throws if > 1MB
validateFileMimeType(file, ["image/jpeg"], "avatar"); // Throws if not JPEGRequirements
- Node.js >= 18.0.0
- Fastify >= 4.0.0 or >= 5.0.0
- @fastify/multipart (peer dependency)
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Support
For issues and questions, please open an issue on the GitHub repository.
