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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aegisx/fastify-multipart

v1.0.1

Published

Production-ready Fastify plugin for multipart/form-data with clean API and full Swagger UI support

Downloads

1,098

Readme

@aegisx/fastify-multipart

Production-ready Fastify plugin for handling multipart/form-data with a clean API and full Swagger UI support. This plugin solves the common issue where text fields become objects with { value: "string" } instead of plain strings, ensuring perfect compatibility with Swagger UI forms.

Features

  • Clean API: Text fields are plain strings, not wrapped objects
  • Full Swagger UI Support: Works perfectly with Swagger UI form submissions
  • Compatible API: Drop-in replacement for @fastify/multipart
  • Automatic Cleanup: Temporary files are cleaned up automatically
  • TypeScript Support: Full TypeScript definitions included
  • Streaming Support: Efficient file handling with streams
  • Configurable Limits: Control file sizes, field counts, and more

Requirements

  • Node.js >= 18
  • Fastify 4.x or 5.x

Installation

npm install @aegisx/fastify-multipart

Quick Start

const fastify = require('fastify')()
const multipart = require('@aegisx/fastify-multipart')

// Register the plugin
await fastify.register(multipart)

// Create an upload route
fastify.post('/upload', async (request, reply) => {
  const { files, fields } = await request.parseMultipart()
  
  // fields.name is a plain string, not { value: "string" }
  console.log('Name:', fields.name)
  console.log('Description:', fields.description)
  
  // Handle uploaded files
  for (const file of files) {
    console.log('File:', file.filename, file.size, 'bytes')
    // Save file or process it
    const buffer = await file.toBuffer()
  }
  
  return { success: true }
})

API Documentation

Plugin Options

await fastify.register(multipart, {
  limits: {
    fileSize: 1024 * 1024 * 10,  // 10MB (default)
    files: 10,                    // Max number of files (default: 10)
    fields: 20,                   // Max number of fields (default: 20)
    fieldNameSize: 100,           // Max field name length (default: 100)
    fieldSize: 1024 * 1024,       // Max field value size (default: 1MB)
    headerPairs: 2000             // Max header pairs (default: 2000)
  },
  tempDir: '/tmp',                // Temp directory (default: os.tmpdir())
  autoContentTypeParser: true     // Auto-register parser (default: true)
})

Request Methods

request.parseMultipart()

Parse multipart form data. Returns a promise with files and fields.

const { files, fields, _tempFiles } = await request.parseMultipart()

// fields are plain strings
console.log(fields.category)     // "electronics"
console.log(fields.description)  // "Product description"

// files array contains file objects
for (const file of files) {
  console.log(file.filename)
  console.log(file.mimetype)
  console.log(file.size)
}

request.file()

Get the first uploaded file or null.

const file = request.file()
if (file) {
  const buffer = await file.toBuffer()
}

request.files()

Get all uploaded files as an array.

const files = request.files()
for (const file of files) {
  const stream = file.createReadStream()
  // Process stream...
}

request.parts()

Get an async iterator for streaming multipart parts.

for await (const part of request.parts()) {
  if (part.type === 'file') {
    // Handle file stream
    console.log('File:', part.filename)
    // part.stream is a readable stream
  } else {
    // Handle field
    console.log('Field:', part.fieldname, part.value)
  }
}

request.cleanupTempFiles()

Manually cleanup temporary files (automatic cleanup happens on response).

await request.cleanupTempFiles()

File Object

Each file object has the following properties and methods:

{
  filename: 'image.jpg',           // Original filename
  encoding: '7bit',                // File encoding
  mimetype: 'image/jpeg',          // MIME type
  size: 102400,                    // Size in bytes (getter)
  toBuffer(): Promise<Buffer>,     // Read file into buffer
  createReadStream(): Readable,    // Create read stream
  _tempPath: '/tmp/upload_xxx'     // Temp file path (internal)
}

Error Handling

The plugin exports error constructors via fastify.multipartErrors:

fastify.post('/upload', async (request, reply) => {
  try {
    const { files, fields } = await request.parseMultipart()
    // Process upload...
  } catch (err) {
    if (err instanceof fastify.multipartErrors.FileSizeLimit) {
      return reply.code(413).send({ error: 'File too large' })
    }
    if (err instanceof fastify.multipartErrors.FilesLimit) {
      return reply.code(413).send({ error: 'Too many files' })
    }
    throw err
  }
})

Swagger UI Integration

This plugin works perfectly with Swagger UI form submissions. Here's the recommended setup:

const fastify = require('fastify')()
const multipart = require('@aegisx/fastify-multipart')
const swagger = require('@fastify/swagger')
const swaggerUI = require('@fastify/swagger-ui')

// Register Swagger first
await fastify.register(swagger, { /* options */ })
await fastify.register(swaggerUI, { /* options */ })

// Register multipart plugin with validation bypass
await fastify.register(multipart, {
  autoContentTypeParser: false // Important!
})

// Custom content type parser
fastify.addContentTypeParser('multipart/form-data', function (request, payload, done) {
  done(null, payload)
})

// Bypass validation for multipart routes (prevents validation errors)
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
  return function validate(data) {
    // Skip body validation for upload routes
    if (httpPart === 'body' && url && url.includes('/upload')) {
      return { value: data }
    }
    return { value: data }
  }
})

fastify.post('/upload/products', {
  schema: {
    summary: 'Create product with image',
    consumes: ['multipart/form-data'],
    body: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        category: { type: 'string' },
        description: { type: 'string' },
        image: { type: 'string', format: 'binary' }
      },
      required: ['name', 'category']
    }
  }
}, async (request, reply) => {
  const { files, fields } = await request.parseMultipart()
  
  // Manual validation since schema validation is bypassed
  if (!fields.name || !fields.category) {
    return reply.code(400).send({ error: 'Name and category are required' })
  }
  
  // Text fields are plain strings - works perfectly with Swagger UI!
  console.log('Name:', fields.name)          // "Product Name"
  console.log('Category:', fields.category)  // "Electronics"
  
  return { success: true, data: fields }
})

Why This Setup?

The custom validator bypass prevents Fastify from trying to validate multipart form data against JSON schemas, which causes the "Value must be a string" errors you might have seen. With this setup:

✅ Swagger UI displays the form correctly
✅ No validation errors
✅ Text fields are plain strings
✅ Perfect user experience

Migration from @fastify/multipart

Migrating from @fastify/multipart is straightforward:

Before (with @fastify/multipart):

const multipart = require('@fastify/multipart')
await fastify.register(multipart, { attachFieldsToBody: true })

fastify.post('/upload', async (request, reply) => {
  // Fields are wrapped objects
  const name = request.body.name.value        // { value: "John" }
  const email = request.body.email.value      // { value: "[email protected]" }
  
  // Files need separate handling
  const files = request.files()
})

After (with @aegisx/fastify-multipart):

const multipart = require('@aegisx/fastify-multipart')
await fastify.register(multipart)

fastify.post('/upload', async (request, reply) => {
  const { files, fields } = await request.parseMultipart()
  
  // Fields are plain strings!
  const name = fields.name      // "John"
  const email = fields.email    // "[email protected]"
  
  // Files are included in the same result
})

Comparison with @fastify/multipart

| Feature | @aegisx/fastify-multipart | @fastify/multipart | |---------|---------------------------|-------------------| | Text fields format | Plain strings ✅ | Wrapped objects { value } | | Swagger UI compatibility | Full support ✅ | Requires workarounds | | API simplicity | Single method returns all ✅ | Multiple methods needed | | TypeScript support | Full definitions ✅ | Full definitions ✅ | | Automatic cleanup | Yes ✅ | Yes ✅ | | Streaming support | Yes ✅ | Yes ✅ | | Field validation | Direct validation ✅ | Complex validation | | Node.js support | >= 18 | >= 14 | | CI/CD tested | Node 18, 20, 22 ✅ | Varies |

TypeScript Usage

import fastify from 'fastify'
import multipart, { MultipartFile, MultipartParseResult } from '@aegisx/fastify-multipart'

const app = fastify()
await app.register(multipart)

app.post('/upload', async (request, reply) => {
  const { files, fields }: MultipartParseResult = await request.parseMultipart()
  
  // TypeScript knows fields are Record<string, string>
  const name: string = fields.name
  
  // TypeScript knows files array structure
  files.forEach((file: MultipartFile) => {
    console.log(file.filename)
  })
  
  return { success: true }
})

Advanced Examples

Handle Large Files with Streaming

fastify.post('/upload-large', async (request, reply) => {
  for await (const part of request.parts()) {
    if (part.type === 'file') {
      // Stream directly to storage instead of loading into memory
      const writeStream = fs.createWriteStream(`./uploads/${part.filename}`)
      await pipeline(part.stream, writeStream)
    }
  }
  return { success: true }
})

Custom Error Handling

fastify.setErrorHandler((error, request, reply) => {
  if (error instanceof fastify.multipartErrors.FileSizeLimit) {
    reply.status(413).send({
      statusCode: 413,
      error: 'Payload Too Large',
      message: `File size limit exceeded: ${error.message}`
    })
  } else {
    reply.send(error)
  }
})

Conditional File Processing

fastify.post('/upload-images', async (request, reply) => {
  const { files, fields } = await request.parseMultipart()
  
  const imageFiles = files.filter(file => 
    file.mimetype.startsWith('image/')
  )
  
  if (imageFiles.length === 0) {
    return reply.code(400).send({ error: 'No images uploaded' })
  }
  
  // Process only image files
  for (const image of imageFiles) {
    const buffer = await image.toBuffer()
    // Process image...
  }
  
  return { processed: imageFiles.length }
})

Troubleshooting

Common Issues

  1. Swagger Validation Error: "Value must be a string"

    This happens when Fastify tries to validate multipart form data against JSON schemas.

    Solution: Use the validation bypass setup shown in the Swagger Integration section:

    await fastify.register(multipart, { autoContentTypeParser: false })
       
    fastify.addContentTypeParser('multipart/form-data', function (request, payload, done) {
      done(null, payload)
    })
       
    fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
      return function validate(data) {
        if (httpPart === 'body' && url && url.includes('/upload')) {
          return { value: data }
        }
        return { value: data }
      }
    })
  2. "Unexpected end of form" Error

    This can happen if the content type parser conflicts with the plugin.

    Solution: Set autoContentTypeParser: false and register manually:

    await fastify.register(multipart, { autoContentTypeParser: false })
  3. File Size Limit Exceeded

    // Increase file size limit
    await fastify.register(multipart, {
      limits: { fileSize: 1024 * 1024 * 50 } // 50MB
    })
  4. Too Many Files

    // Increase file count limit
    await fastify.register(multipart, {
      limits: { files: 20 }
    })
  5. Field Value Too Large

    // Increase field size limit
    await fastify.register(multipart, {
      limits: { fieldSize: 1024 * 1024 * 5 } // 5MB
    })

Debug Mode

Enable debug logging to troubleshoot issues:

const fastify = require('fastify')({ logger: true })

Quick Test

Use this simple test to verify the plugin works:

const fastify = require('fastify')()
const multipart = require('@aegisx/fastify-multipart')

await fastify.register(multipart, { autoContentTypeParser: false })
fastify.addContentTypeParser('multipart/form-data', (req, payload, done) => done(null, payload))

fastify.post('/test', async (request) => {
  const { fields, files } = await request.parseMultipart()
  return { fieldsType: typeof fields.name, filesCount: files.length }
})

// Test with curl:
// curl -X POST http://localhost:3000/test -F "name=test" -F "[email protected]"

Development

Requirements

  • Node.js >= 18
  • npm or yarn
  • Docker (optional, for matrix testing)

Setup

# Clone the repository
git clone https://github.com/aegisx-platform/fastify-multipart.git
cd fastify-multipart

# Install dependencies
npm install

# Run tests
npm test

# Run linting
npm run lint

# Run examples
npm run example:basic
npm run example:swagger
npm run example:complete

Testing

The plugin is tested against multiple Node.js versions and Fastify versions:

  • Node.js: 18, 20, 22
  • Fastify: 4.x, 5.x

Local Matrix Testing

Test against different Node.js versions locally using Docker:

# Test with Node 18
./test-node-18.sh

# Test all combinations (requires Docker)
./test-matrix.sh

# Test with nvm (requires nvm installed)
./test-matrix-nvm.sh

Manual Testing

# Test with specific Node version using Docker
docker run --rm -v "$(pwd)":/app -w /app node:18-alpine sh -c "npm ci && npm test"
docker run --rm -v "$(pwd)":/app -w /app node:20-alpine sh -c "npm ci && npm test"

# Test with specific Fastify version
npm install [email protected] && npm test
npm install [email protected] && npm test

CI/CD

The project uses GitHub Actions for continuous integration:

  • CI: Runs on every push and pull request
  • Matrix Testing: Tests against Node.js 18, 20, 22 with Fastify 4.x and 5.x
  • Security Audit: Checks for vulnerabilities
  • Semantic Release: Automated version management and publishing

Commit Convention

This project follows Conventional Commits:

# Format
<type>(<scope>): <subject>

# Examples
feat(plugin): add support for custom temp directory
fix(multipart): resolve file size limit error handling
docs(readme): update installation instructions
chore(deps): update fastify to v5

Types: feat, fix, docs, style, refactor, test, chore Scopes: plugin, multipart, swagger, examples, docs, tests, ci, deps

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat(plugin): add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT License - see LICENSE file for details.

Support