5 min read

How to upload a file in Next.js 13+ App Directory with No libraries

How to upload a file in Next.js 13+ App Directory with No libraries
Photo by Markus Winkler / Unsplash

Uploading a file with Next.js 13 can be done with no third-party libraries letting you move fast and keep bundle size down. You can do this using client components or React Server components. The trick is to manipulate the file upload into the correct data type so it plays nicely with Node.js and whatever library or API wants to use the file.

Client Component and API Route

If your form is a client component, you should save the file in state and then upload it using a fetch request.

Client Component

'use client'

import { useState } from 'react'

export function UploadForm() {
  const [file, setFile] = useState<File>()

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    if (!file) return

    try {
      const data = new FormData()
      data.set('file', file)

      const res = await fetch('/api/upload', {
        method: 'POST',
        body: data
      })
      // handle the error
      if (!res.ok) throw new Error(await res.text())
    } catch (e: any) {
      // Handle errors here
      console.error(e)
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input
        type="file"
        name="file"
        onChange={(e) => setFile(e.target.files?.[0])}
      />
      <input type="submit" value="Upload" />
    </form>
  )
}

This code defines a React functional component called UploadForm. The component renders a form with a file input field and a submit button, allowing users to upload a file to a server. Here are the key parts:

The useState hook is used to create a state variable file and its corresponding update function setFile.

The onSubmit function is an asynchronous event handler for the form submission. It performs the following actions:

  1. Prevents the default form submission behavior.
  2. Checks if a file is selected. If not, it returns early.
  3. Creates a FormData object and appends the selected file to it.
  4. Sends an HTTP POST request to the /api/upload endpoint with the file as its body.
  5. If the response is not successful, it throws an error with the response text.
  6. Catches any errors that occur and logs them to the console.

Note that you do not need to set the header Content-Type to be multipart/form-data. This is done automatically when sending FormData.

The return statement renders the form with the following elements:

  • A file input field, which sets the file state variable when a file is selected.
  • A submit button that triggers the form submission and calls the onSubmit function.

When a user selects a file and clicks the "Upload" button, the onSubmit function sends the file to the server.

This is pretty standard React code. But how do we handle this on the server and do something with the file?

Next.js API Route

To handle the form upload, you will need to create an API route that consumes the data and does something with it.

Create the file app/api/upload/route.ts and add the following code:

import { writeFile } from 'fs/promises'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const data = await request.formData()
  const file: File | null = data.get('file') as unknown as File

  if (!file) {
    return NextResponse.json({ success: false })
  }

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

  // With the file data in the buffer, you can do whatever you want with it.
  // For this, we'll just write it to the filesystem in a new location
  const path = `/tmp/${file.name}`
  await writeFile(path, buffer)
  console.log(`open ${path} to see the uploaded file`)

  return NextResponse.json({ success: true })
}

This code defines an asynchronous function POST that handles an HTTP POST request containing a file, reads the file, and then writes it to the server's local filesystem. Here are the key parts:

  1. The POST function takes a request parameter of type NextRequest. It starts by reading the form data from the request using the await request.formData() method.
  2. Extract the uploaded File object from the form data using data.get('file'). If no file is found, it returns a JSON response with { success: false }.
  3. Read the file content as an ArrayBuffer using await file.arrayBuffer() and then convert it into a Node.js Buffer object using Buffer.from(bytes). This is key. It turns the data from a Web API object into a Node.js Buffer, allowing you to handle the data easily.
  4. For this demo, with the file data in the buffer, the code writes the file to a new location in the server's local filesystem using the writeFile function. The new file path is set to /tmp/${file.name}.
  5. If everything is successful, return a JSON response with { success: true }.

When a client sends a file via an HTTP POST request to this API route, this code reads the file, saves it to the server's filesystem, and returns a JSON response indicating success or failure.

React Server Components

By using the experimental server actions in Next.js 13, you can combine the two above pieces of code into a single elegant React Server Component. Let's take a look at how it looks.

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

export default function ServerUploadPage() {
  async function upload(data: FormData) {
    'use server'

    const file: File | null = data.get('file') as unknown as File
    if (!file) {
      throw new Error('No file uploaded')
    }

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

    // With the file data in the buffer, you can do whatever you want with it.
    // For this, we'll just write it to the filesystem in a new location
    const path = join('/', 'tmp', file.name)
    await writeFile(path, buffer)
    console.log(`open ${path} to see the uploaded file`)

    return { success: true }
  }

  return (
    <main>
      <h1>React Server Component: Upload</h1>
      <form action={upload}>
        <input type="file" name="file" />
        <input type="submit" value="Upload" />
      </form>
    </main>
  )
}

That's all you need to do! The form takes an action property that is the server action. That action handles the file upload just like the API route does, but you can inline the entire function right in the component. By doing it this way, your form is functional without JavaScript, and there is less data sent to the client.

Code

You can grab the full sample code below, including both the client and server ways of handling file upload.

examples/next/file-upload at main · a-bit-of-saas/examples
Example code for learning. Contribute to a-bit-of-saas/examples development by creating an account on GitHub.