{"_id":"@aegisx/fastify-multipart","_rev":"2-a972f0aa26e176663a9a2793acbe6905","name":"@aegisx/fastify-multipart","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@aegisx/fastify-multipart","version":"1.0.0","keywords":["fastify","fastify-plugin","multipart","file-upload","swagger","swagger-ui","busboy","form-data","clean-api","aegisx"],"author":{"name":"Sathit Seethaphon","email":"dixonsatit@gmail.com"},"license":"MIT","_id":"@aegisx/fastify-multipart@1.0.0","maintainers":[{"name":"dixonsatit","email":"dixonsatit@gmail.com"}],"homepage":"https://github.com/aegisx-platform/fastify-multipart#readme","bugs":{"url":"https://github.com/aegisx-platform/fastify-multipart/issues"},"dist":{"shasum":"a8f75a2c0a276703dd98fd99b6709f7383d38345","tarball":"https://registry.npmjs.org/@aegisx/fastify-multipart/-/fastify-multipart-1.0.0.tgz","fileCount":6,"integrity":"sha512-t4b9/RUKaYKQllipNdkW94f8/tOe4d8sVEpbZTtQ1Yuxw4qiAAvk8PbebIIQm0QMhwUhK8Dwt0FoxZKKdK420Q==","signatures":[{"sig":"MEUCIFpRGPomQOTU4JOSd122CPkWOfQ4BkpP+RB3rXE1jBkfAiEA64+JmtoqnW0hh/XjLHLB4bfTU/ypiz+qU2tDCQGNK5A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":27051},"main":"index.js","types":"index.d.ts","engines":{"node":">=14"},"gitHead":"2b4dfcabbaf5e396e7c5af4e38c7dd948b8bf239","scripts":{"lint":"standard","test":"tap test/*.test.js","publish":"npm publish --access public","lint:fix":"standard --fix","test:watch":"tap test/*.test.js --watch","example:basic":"node examples/basic-usage.js","example:errors":"node examples/error-handling.js","example:swagger":"node examples/swagger-integration.js","example:complete":"node examples/complete-usage-example.js","example:streaming":"node examples/streaming-upload.js"},"_npmUser":{"name":"dixonsatit","actor":{"name":"dixonsatit","type":"user","email":"dixonsatit@gmail.com"},"email":"dixonsatit@gmail.com"},"standard":{"ignore":["test/**"]},"repository":{"url":"git+https://github.com/aegisx-platform/fastify-multipart.git","type":"git"},"_npmVersion":"10.9.0","description":"Production-ready Fastify plugin for multipart/form-data with clean API and full Swagger UI support","directories":{},"_nodeVersion":"22.11.0","dependencies":{"busboy":"^1.6.0","@fastify/error":"^3.4.0","fastify-plugin":"^4.5.0"},"_hasShrinkwrap":false,"devDependencies":{"tap":"^18.5.0","undici":"^6.2.0","fastify":"^4.25.0","standard":"^17.1.0","form-data":"^4.0.0","@fastify/swagger":"^8.12.0","@fastify/swagger-ui":"^2.0.0"},"peerDependencies":{"fastify":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/fastify-multipart_1.0.0_1751393260197_0.7154910328847783","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@aegisx/fastify-multipart","version":"1.0.1","description":"Production-ready Fastify plugin for multipart/form-data with clean API and full Swagger UI support","main":"index.js","types":"index.d.ts","scripts":{"test":"tap test/*.test.js --disable-coverage","test:watch":"tap test/*.test.js --watch","lint":"standard","lint:fix":"standard --fix","example:basic":"node examples/basic-usage.js","example:swagger":"node examples/swagger-integration.js","example:streaming":"node examples/streaming-upload.js","example:errors":"node examples/error-handling.js","example:complete":"node examples/complete-usage-example.js","semantic-release":"semantic-release","publish":"npm publish --access public","prepare":"husky install","commitlint":"commitlint --edit"},"keywords":["fastify","fastify-plugin","multipart","file-upload","swagger","swagger-ui","busboy","form-data","clean-api","aegisx"],"author":{"name":"Sathit Seethaphon","email":"dixonsatit@gmail.com"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/aegisx-platform/fastify-multipart.git"},"bugs":{"url":"https://github.com/aegisx-platform/fastify-multipart/issues"},"homepage":"https://github.com/aegisx-platform/fastify-multipart#readme","dependencies":{"@fastify/error":"^3.4.0","busboy":"^1.6.0","fastify-plugin":"^4.5.0"},"devDependencies":{"@commitlint/cli":"^19.8.1","@commitlint/config-conventional":"^19.8.1","@fastify/swagger":"^8.12.0","@fastify/swagger-ui":"^2.0.0","@semantic-release/changelog":"^6.0.3","@semantic-release/git":"^10.0.1","@semantic-release/github":"^11.0.3","@semantic-release/npm":"^12.0.2","fastify":"^4.29.1","form-data":"^4.0.0","husky":"^9.1.7","semantic-release":"^24.2.6","standard":"^17.1.0","tap":"^18.5.0","undici":"^6.2.0"},"peerDependencies":{"fastify":"^4.0.0 || ^5.0.0"},"engines":{"node":">=18"},"standard":{"ignore":["test/**"]},"_id":"@aegisx/fastify-multipart@1.0.1","gitHead":"347c37d1ed07eaad934c67368d5e55421ab49f6e","_nodeVersion":"20.19.2","_npmVersion":"10.9.3","dist":{"integrity":"sha512-tQ3MU7R2duN69Ya4ySeuTgSR6usqq0OJy/tiqDWVVX1JntNP9R8ABR4y021h3ob1G5rFeps7KELQQhvURw0Wgg==","shasum":"53acc9b4a64ea7f723ff19a3b346b2504fe44f07","tarball":"https://registry.npmjs.org/@aegisx/fastify-multipart/-/fastify-multipart-1.0.1.tgz","fileCount":15,"unpackedSize":45508,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEuxCzDKFd+dGc68tRDcMeJkLjtFQ0PcEP9shC72tzngAiEAldeiGc/i1Rs2rB8A4L3kqP1mpWSAAfIYoDO3uSUkFUs="}]},"_npmUser":{"name":"dixonsatit","email":"dixonsatit@gmail.com","actor":{"name":"dixonsatit","email":"dixonsatit@gmail.com","type":"user"}},"directories":{},"maintainers":[{"name":"dixonsatit","email":"dixonsatit@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fastify-multipart_1.0.1_1751476145059_0.18512727614391777"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-01T18:07:40.101Z","modified":"2025-07-02T17:09:05.413Z","1.0.0":"2025-07-01T18:07:40.418Z","1.0.1":"2025-07-02T17:09:05.235Z"},"bugs":{"url":"https://github.com/aegisx-platform/fastify-multipart/issues"},"author":{"name":"Sathit Seethaphon","email":"dixonsatit@gmail.com"},"license":"MIT","homepage":"https://github.com/aegisx-platform/fastify-multipart#readme","keywords":["fastify","fastify-plugin","multipart","file-upload","swagger","swagger-ui","busboy","form-data","clean-api","aegisx"],"repository":{"type":"git","url":"git+https://github.com/aegisx-platform/fastify-multipart.git"},"description":"Production-ready Fastify plugin for multipart/form-data with clean API and full Swagger UI support","maintainers":[{"name":"dixonsatit","email":"dixonsatit@gmail.com"}],"readme":"# @aegisx/fastify-multipart\n\nProduction-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.\n\n## Features\n\n- ✅ **Clean API**: Text fields are plain strings, not wrapped objects\n- ✅ **Full Swagger UI Support**: Works perfectly with Swagger UI form submissions\n- ✅ **Compatible API**: Drop-in replacement for @fastify/multipart\n- ✅ **Automatic Cleanup**: Temporary files are cleaned up automatically\n- ✅ **TypeScript Support**: Full TypeScript definitions included\n- ✅ **Streaming Support**: Efficient file handling with streams\n- ✅ **Configurable Limits**: Control file sizes, field counts, and more\n\n## Requirements\n\n- Node.js >= 18\n- Fastify 4.x or 5.x\n\n## Installation\n\n```bash\nnpm install @aegisx/fastify-multipart\n```\n\n## Quick Start\n\n```javascript\nconst fastify = require('fastify')()\nconst multipart = require('@aegisx/fastify-multipart')\n\n// Register the plugin\nawait fastify.register(multipart)\n\n// Create an upload route\nfastify.post('/upload', async (request, reply) => {\n  const { files, fields } = await request.parseMultipart()\n  \n  // fields.name is a plain string, not { value: \"string\" }\n  console.log('Name:', fields.name)\n  console.log('Description:', fields.description)\n  \n  // Handle uploaded files\n  for (const file of files) {\n    console.log('File:', file.filename, file.size, 'bytes')\n    // Save file or process it\n    const buffer = await file.toBuffer()\n  }\n  \n  return { success: true }\n})\n```\n\n## API Documentation\n\n### Plugin Options\n\n```javascript\nawait fastify.register(multipart, {\n  limits: {\n    fileSize: 1024 * 1024 * 10,  // 10MB (default)\n    files: 10,                    // Max number of files (default: 10)\n    fields: 20,                   // Max number of fields (default: 20)\n    fieldNameSize: 100,           // Max field name length (default: 100)\n    fieldSize: 1024 * 1024,       // Max field value size (default: 1MB)\n    headerPairs: 2000             // Max header pairs (default: 2000)\n  },\n  tempDir: '/tmp',                // Temp directory (default: os.tmpdir())\n  autoContentTypeParser: true     // Auto-register parser (default: true)\n})\n```\n\n### Request Methods\n\n#### `request.parseMultipart()`\n\nParse multipart form data. Returns a promise with files and fields.\n\n```javascript\nconst { files, fields, _tempFiles } = await request.parseMultipart()\n\n// fields are plain strings\nconsole.log(fields.category)     // \"electronics\"\nconsole.log(fields.description)  // \"Product description\"\n\n// files array contains file objects\nfor (const file of files) {\n  console.log(file.filename)\n  console.log(file.mimetype)\n  console.log(file.size)\n}\n```\n\n#### `request.file()`\n\nGet the first uploaded file or null.\n\n```javascript\nconst file = request.file()\nif (file) {\n  const buffer = await file.toBuffer()\n}\n```\n\n#### `request.files()`\n\nGet all uploaded files as an array.\n\n```javascript\nconst files = request.files()\nfor (const file of files) {\n  const stream = file.createReadStream()\n  // Process stream...\n}\n```\n\n#### `request.parts()`\n\nGet an async iterator for streaming multipart parts.\n\n```javascript\nfor await (const part of request.parts()) {\n  if (part.type === 'file') {\n    // Handle file stream\n    console.log('File:', part.filename)\n    // part.stream is a readable stream\n  } else {\n    // Handle field\n    console.log('Field:', part.fieldname, part.value)\n  }\n}\n```\n\n#### `request.cleanupTempFiles()`\n\nManually cleanup temporary files (automatic cleanup happens on response).\n\n```javascript\nawait request.cleanupTempFiles()\n```\n\n### File Object\n\nEach file object has the following properties and methods:\n\n```javascript\n{\n  filename: 'image.jpg',           // Original filename\n  encoding: '7bit',                // File encoding\n  mimetype: 'image/jpeg',          // MIME type\n  size: 102400,                    // Size in bytes (getter)\n  toBuffer(): Promise<Buffer>,     // Read file into buffer\n  createReadStream(): Readable,    // Create read stream\n  _tempPath: '/tmp/upload_xxx'     // Temp file path (internal)\n}\n```\n\n### Error Handling\n\nThe plugin exports error constructors via `fastify.multipartErrors`:\n\n```javascript\nfastify.post('/upload', async (request, reply) => {\n  try {\n    const { files, fields } = await request.parseMultipart()\n    // Process upload...\n  } catch (err) {\n    if (err instanceof fastify.multipartErrors.FileSizeLimit) {\n      return reply.code(413).send({ error: 'File too large' })\n    }\n    if (err instanceof fastify.multipartErrors.FilesLimit) {\n      return reply.code(413).send({ error: 'Too many files' })\n    }\n    throw err\n  }\n})\n```\n\n## Swagger UI Integration\n\nThis plugin works perfectly with Swagger UI form submissions. Here's the recommended setup:\n\n```javascript\nconst fastify = require('fastify')()\nconst multipart = require('@aegisx/fastify-multipart')\nconst swagger = require('@fastify/swagger')\nconst swaggerUI = require('@fastify/swagger-ui')\n\n// Register Swagger first\nawait fastify.register(swagger, { /* options */ })\nawait fastify.register(swaggerUI, { /* options */ })\n\n// Register multipart plugin with validation bypass\nawait fastify.register(multipart, {\n  autoContentTypeParser: false // Important!\n})\n\n// Custom content type parser\nfastify.addContentTypeParser('multipart/form-data', function (request, payload, done) {\n  done(null, payload)\n})\n\n// Bypass validation for multipart routes (prevents validation errors)\nfastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {\n  return function validate(data) {\n    // Skip body validation for upload routes\n    if (httpPart === 'body' && url && url.includes('/upload')) {\n      return { value: data }\n    }\n    return { value: data }\n  }\n})\n\nfastify.post('/upload/products', {\n  schema: {\n    summary: 'Create product with image',\n    consumes: ['multipart/form-data'],\n    body: {\n      type: 'object',\n      properties: {\n        name: { type: 'string' },\n        category: { type: 'string' },\n        description: { type: 'string' },\n        image: { type: 'string', format: 'binary' }\n      },\n      required: ['name', 'category']\n    }\n  }\n}, async (request, reply) => {\n  const { files, fields } = await request.parseMultipart()\n  \n  // Manual validation since schema validation is bypassed\n  if (!fields.name || !fields.category) {\n    return reply.code(400).send({ error: 'Name and category are required' })\n  }\n  \n  // Text fields are plain strings - works perfectly with Swagger UI!\n  console.log('Name:', fields.name)          // \"Product Name\"\n  console.log('Category:', fields.category)  // \"Electronics\"\n  \n  return { success: true, data: fields }\n})\n```\n\n### Why This Setup?\n\nThe 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:\n\n✅ Swagger UI displays the form correctly  \n✅ No validation errors  \n✅ Text fields are plain strings  \n✅ Perfect user experience\n\n## Migration from @fastify/multipart\n\nMigrating from `@fastify/multipart` is straightforward:\n\n### Before (with @fastify/multipart):\n```javascript\nconst multipart = require('@fastify/multipart')\nawait fastify.register(multipart, { attachFieldsToBody: true })\n\nfastify.post('/upload', async (request, reply) => {\n  // Fields are wrapped objects\n  const name = request.body.name.value        // { value: \"John\" }\n  const email = request.body.email.value      // { value: \"john@email.com\" }\n  \n  // Files need separate handling\n  const files = request.files()\n})\n```\n\n### After (with @aegisx/fastify-multipart):\n```javascript\nconst multipart = require('@aegisx/fastify-multipart')\nawait fastify.register(multipart)\n\nfastify.post('/upload', async (request, reply) => {\n  const { files, fields } = await request.parseMultipart()\n  \n  // Fields are plain strings!\n  const name = fields.name      // \"John\"\n  const email = fields.email    // \"john@email.com\"\n  \n  // Files are included in the same result\n})\n```\n\n## Comparison with @fastify/multipart\n\n| Feature | @aegisx/fastify-multipart | @fastify/multipart |\n|---------|---------------------------|-------------------|\n| Text fields format | Plain strings ✅ | Wrapped objects `{ value }` |\n| Swagger UI compatibility | Full support ✅ | Requires workarounds |\n| API simplicity | Single method returns all ✅ | Multiple methods needed |\n| TypeScript support | Full definitions ✅ | Full definitions ✅ |\n| Automatic cleanup | Yes ✅ | Yes ✅ |\n| Streaming support | Yes ✅ | Yes ✅ |\n| Field validation | Direct validation ✅ | Complex validation |\n| Node.js support | >= 18 | >= 14 |\n| CI/CD tested | Node 18, 20, 22 ✅ | Varies |\n\n## TypeScript Usage\n\n```typescript\nimport fastify from 'fastify'\nimport multipart, { MultipartFile, MultipartParseResult } from '@aegisx/fastify-multipart'\n\nconst app = fastify()\nawait app.register(multipart)\n\napp.post('/upload', async (request, reply) => {\n  const { files, fields }: MultipartParseResult = await request.parseMultipart()\n  \n  // TypeScript knows fields are Record<string, string>\n  const name: string = fields.name\n  \n  // TypeScript knows files array structure\n  files.forEach((file: MultipartFile) => {\n    console.log(file.filename)\n  })\n  \n  return { success: true }\n})\n```\n\n## Advanced Examples\n\n### Handle Large Files with Streaming\n\n```javascript\nfastify.post('/upload-large', async (request, reply) => {\n  for await (const part of request.parts()) {\n    if (part.type === 'file') {\n      // Stream directly to storage instead of loading into memory\n      const writeStream = fs.createWriteStream(`./uploads/${part.filename}`)\n      await pipeline(part.stream, writeStream)\n    }\n  }\n  return { success: true }\n})\n```\n\n### Custom Error Handling\n\n```javascript\nfastify.setErrorHandler((error, request, reply) => {\n  if (error instanceof fastify.multipartErrors.FileSizeLimit) {\n    reply.status(413).send({\n      statusCode: 413,\n      error: 'Payload Too Large',\n      message: `File size limit exceeded: ${error.message}`\n    })\n  } else {\n    reply.send(error)\n  }\n})\n```\n\n### Conditional File Processing\n\n```javascript\nfastify.post('/upload-images', async (request, reply) => {\n  const { files, fields } = await request.parseMultipart()\n  \n  const imageFiles = files.filter(file => \n    file.mimetype.startsWith('image/')\n  )\n  \n  if (imageFiles.length === 0) {\n    return reply.code(400).send({ error: 'No images uploaded' })\n  }\n  \n  // Process only image files\n  for (const image of imageFiles) {\n    const buffer = await image.toBuffer()\n    // Process image...\n  }\n  \n  return { processed: imageFiles.length }\n})\n```\n\n## Troubleshooting\n\n### Common Issues\n\n1. **Swagger Validation Error: \"Value must be a string\"**\n   \n   This happens when Fastify tries to validate multipart form data against JSON schemas.\n   \n   **Solution:** Use the validation bypass setup shown in the Swagger Integration section:\n   ```javascript\n   await fastify.register(multipart, { autoContentTypeParser: false })\n   \n   fastify.addContentTypeParser('multipart/form-data', function (request, payload, done) {\n     done(null, payload)\n   })\n   \n   fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {\n     return function validate(data) {\n       if (httpPart === 'body' && url && url.includes('/upload')) {\n         return { value: data }\n       }\n       return { value: data }\n     }\n   })\n   ```\n\n2. **\"Unexpected end of form\" Error**\n   \n   This can happen if the content type parser conflicts with the plugin.\n   \n   **Solution:** Set `autoContentTypeParser: false` and register manually:\n   ```javascript\n   await fastify.register(multipart, { autoContentTypeParser: false })\n   ```\n\n3. **File Size Limit Exceeded**\n   ```javascript\n   // Increase file size limit\n   await fastify.register(multipart, {\n     limits: { fileSize: 1024 * 1024 * 50 } // 50MB\n   })\n   ```\n\n4. **Too Many Files**\n   ```javascript\n   // Increase file count limit\n   await fastify.register(multipart, {\n     limits: { files: 20 }\n   })\n   ```\n\n5. **Field Value Too Large**\n   ```javascript\n   // Increase field size limit\n   await fastify.register(multipart, {\n     limits: { fieldSize: 1024 * 1024 * 5 } // 5MB\n   })\n   ```\n\n### Debug Mode\n\nEnable debug logging to troubleshoot issues:\n\n```javascript\nconst fastify = require('fastify')({ logger: true })\n```\n\n### Quick Test\n\nUse this simple test to verify the plugin works:\n\n```javascript\nconst fastify = require('fastify')()\nconst multipart = require('@aegisx/fastify-multipart')\n\nawait fastify.register(multipart, { autoContentTypeParser: false })\nfastify.addContentTypeParser('multipart/form-data', (req, payload, done) => done(null, payload))\n\nfastify.post('/test', async (request) => {\n  const { fields, files } = await request.parseMultipart()\n  return { fieldsType: typeof fields.name, filesCount: files.length }\n})\n\n// Test with curl:\n// curl -X POST http://localhost:3000/test -F \"name=test\" -F \"file=@package.json\"\n```\n\n## Development\n\n### Requirements\n\n- Node.js >= 18\n- npm or yarn\n- Docker (optional, for matrix testing)\n\n### Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/aegisx-platform/fastify-multipart.git\ncd fastify-multipart\n\n# Install dependencies\nnpm install\n\n# Run tests\nnpm test\n\n# Run linting\nnpm run lint\n\n# Run examples\nnpm run example:basic\nnpm run example:swagger\nnpm run example:complete\n```\n\n### Testing\n\nThe plugin is tested against multiple Node.js versions and Fastify versions:\n\n- **Node.js**: 18, 20, 22\n- **Fastify**: 4.x, 5.x\n\n#### Local Matrix Testing\n\nTest against different Node.js versions locally using Docker:\n\n```bash\n# Test with Node 18\n./test-node-18.sh\n\n# Test all combinations (requires Docker)\n./test-matrix.sh\n\n# Test with nvm (requires nvm installed)\n./test-matrix-nvm.sh\n```\n\n#### Manual Testing\n\n```bash\n# Test with specific Node version using Docker\ndocker run --rm -v \"$(pwd)\":/app -w /app node:18-alpine sh -c \"npm ci && npm test\"\ndocker run --rm -v \"$(pwd)\":/app -w /app node:20-alpine sh -c \"npm ci && npm test\"\n\n# Test with specific Fastify version\nnpm install fastify@4.x && npm test\nnpm install fastify@5.x && npm test\n```\n\n### CI/CD\n\nThe project uses GitHub Actions for continuous integration:\n\n- **CI**: Runs on every push and pull request\n- **Matrix Testing**: Tests against Node.js 18, 20, 22 with Fastify 4.x and 5.x\n- **Security Audit**: Checks for vulnerabilities\n- **Semantic Release**: Automated version management and publishing\n\n### Commit Convention\n\nThis project follows [Conventional Commits](https://www.conventionalcommits.org/):\n\n```bash\n# Format\n<type>(<scope>): <subject>\n\n# Examples\nfeat(plugin): add support for custom temp directory\nfix(multipart): resolve file size limit error handling\ndocs(readme): update installation instructions\nchore(deps): update fastify to v5\n```\n\n**Types**: feat, fix, docs, style, refactor, test, chore\n**Scopes**: plugin, multipart, swagger, examples, docs, tests, ci, deps\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'feat(plugin): add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n## Support\n\n- 🐛 [Report bugs](https://github.com/aegisx-platform/fastify-multipart/issues)\n- 💡 [Request features](https://github.com/aegisx-platform/fastify-multipart/issues)\n- 📖 [Read documentation](https://github.com/aegisx-platform/fastify-multipart#readme)\n\n","readmeFilename":"README.md"}