How to upload files in a Next.js form

By

Upload files in Next.js 16 with an App Router route handler and request.formData(), plus a short note on the older Pages API multiparty approach.

~~~

To upload files from a Next.js form you need two things: a form that sends multipart/form-data, and a server endpoint that can read that body.

On Next.js 16 with the App Router, that endpoint is a route handler under app/. You call request.formData() and you’re done. No extra parser package.

Here’s the form side. Same idea as always:

<form method="post" action="/api/new" enctype="multipart/form-data">
  <input name="title" type="text" />
  <input name="logo" type="file" />
  <button type="submit">Upload</button>
</form>

App Router: request.formData()

Create app/api/new/route.js (or .ts):

import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'

export async function POST(request) {
  const formData = await request.formData()

  const title = formData.get('title')
  const logo = formData.get('logo')

  if (!logo || typeof logo === 'string') {
    return Response.json({ error: 'Missing file' }, { status: 400 })
  }

  const bytes = await logo.arrayBuffer()
  const buffer = Buffer.from(bytes)

  await writeFile(join(process.cwd(), 'uploads', logo.name), buffer)

  return Response.json({ ok: true, title, filename: logo.name })
}

formData.get('logo') returns a File when the input was a file field. Text fields come back as strings. Multiple values for the same name use formData.getAll('name').

Create the uploads folder in the project root first. writeFile() does not create it for you.

On a new Next.js 16 project, this is all you need.

Pages Router (legacy)

If you’re still on the pages/api API routes, Next.js runs a body parser on every request by default. It handles JSON and URL-encoded data, but not multipart/form-data. So the file never shows up in req.body.

What worked for me on Pages: disable the default body parser, and let a dedicated multipart parser read the raw request stream.

npm install next-connect@0 multiparty

multiparty parses the multipart stream. It writes each uploaded file to a temporary folder on disk, and gives you the form fields and the files metadata. next-connect lets us plug it in as middleware. The code below uses the next-connect 0.x API. Version 1.0 replaced nextConnect() with createRouter(), so pin the old major.

middleware/middleware.js

import nextConnect from 'next-connect'
import multiparty from 'multiparty'

const middleware = nextConnect()

middleware.use(async (req, res, next) => {
  const form = new multiparty.Form()

  await form.parse(req, function (err, fields, files) {
    req.body = fields
    req.files = files
    next()
  })
})

export default middleware

Then the API route:

import middleware from 'middleware/middleware'
import nextConnect from 'next-connect'

const handler = nextConnect()
handler.use(middleware)

handler.post(async (req, res) => {
  console.log(req.body)
  console.log(req.files)

  //...
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default handler

multiparty wraps every value in an array, even when there’s a single one. A text field called title is at req.body.title[0], and a logo file is at req.files.logo[0] with originalFilename, size, and path (the temp file on disk).

The config export with bodyParser: false is not optional on Pages. If you leave it out, Next.js consumes the request body before multiparty gets a chance to read the stream. Put that export in the API route file itself. Putting it in the middleware file does nothing.

Prefer the App Router formData() approach when you can. Keep the Pages/next-connect setup only for older apps that haven’t moved yet.

Tagged: Next.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about next: