How to upload a file in Next.js 13+ App Directory with No libraries
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:
- Prevents the default form submission behavior.
- Checks if a file is selected. If not, it returns early.
- Creates a
FormData
object and appends the selected file to it. - Sends an HTTP POST request to the
/api/upload
endpoint with the file as its body. - If the response is not successful, it throws an error with the response text.
- 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:
- The
POST
function takes arequest
parameter of typeNextRequest
. It starts by reading the form data from the request using theawait request.formData()
method. - Extract the uploaded
File
object from the form data usingdata.get('file')
. If no file is found, it returns a JSON response with{ success: false }
. - Read the file content as an ArrayBuffer using
await file.arrayBuffer()
and then convert it into a Node.jsBuffer
object usingBuffer.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. - 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 thewriteFile
function. The new file path is set to/tmp/${file.name}
. - 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.