npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

fastify-multipart-file

v1.0.2

Published

Fastify plugin for handling multipart/form-data with file validation, type coercion, and nested object support

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-file

Note: 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:

  1. 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
  2. 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 types

Type Coercion Schema

The plugin automatically converts values based on the schema type:

  • S.number() → Converts to number
  • S.integer() → Converts to integer
  • S.boolean() → Converts to boolean ('true', '1' → true)
  • S.object() → Parses JSON string to object
  • S.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 accept list

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 JPEG

Requirements

  • 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.