NextJs File Upload
In this blog post, we will look at how to upload file in NextJs using Server Actions.
Traditionally to handle File uploads, you have to deal with content-type of multipart/formdata. For frameworks like expresses, you have to setup middleware like multer to handle multipart/formdata. With NextJS 13 Server Actions, there is no setup to handle multipart/formdata, it works out of the box. You just post standard form and read formdata on server. It just works!
Lets code File Upload with Server Actions:
Content
Stater Project
Create a NextJS app using
npx create-next-app@latest
npx create-next-app@latest
Name your project "file-upload" and accept rest of the defaults.
Display Profile
First create an uploads directory under public
folder. We will use uploads
directory to store user uploaded photos. Create a new folder called profile under "app" dir (app/profile
). Add a page.tsx
file and copy following
const item = {
first: 'Homer',
last: 'Simpson',
email: 'homer.simpson@sfnpp.com',
title: 'Nuclear Safety Inspector',
photo: 'donut.jpg',
};
async function getData() {
return item;
}
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div className="flex flex-col gap-2">
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img
src={`/uploads/${data?.photo}`}
alt={data?.first}
className="w-48 "
/>
</div>
</>
);
}
const item = {
first: 'Homer',
last: 'Simpson',
email: 'homer.simpson@sfnpp.com',
title: 'Nuclear Safety Inspector',
photo: 'donut.jpg',
};
async function getData() {
return item;
}
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div className="flex flex-col gap-2">
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img
src={`/uploads/${data?.photo}`}
alt={data?.first}
className="w-48 "
/>
</div>
</>
);
}
Here we are using getData
to load user profile data and display UI.
Start project with npm run dev
and go to http://localhost:3000/profile
You should see Homer's profile. Notice Profile photo is missing as we still haven't uploaded it. Lets do that next.
File Upload
We will let user select a photo and upload it. Once uploaded, we will store photo locally on the server in uploads
directory. (you could of-course save it to a CDN location as well)
Modify page.tsx
as shown
import path from "path";
import fs from "fs";
import { revalidatePath } from "next/cache";
const item = {
first: 'Homer',
last: 'Simpson',
email: 'homer.simpson@sfnpp.com',
title: 'Nuclear Safety Inspector',
photo: 'donut.jpg',
};
async function uploadFile(formData: FormData) {
"use server";
const file = formData.get("file") as File;
//console.log("File name:", file.name, "size:", file.size);
if (file.size) {
const fileName = file?.name;
const extension = fileName?.split(".").pop();
let imageName = `${Date.now()}.${extension ?? "jpg"}`;// file.name;
item.photo = imageName;
const imagePath = path.join(`./public/uploads/`, imageName);
const imageStream = fs.createWriteStream(imagePath);
imageStream.write(Buffer.from(await file.arrayBuffer()));
imageStream.end();
}
revalidatePath("/profile");
}
async function getData(barcode: string) {
return item;
}
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div>
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img src={`/uploads/${data?.photo}`} alt={data?.first} className="w-48 " />
</div>
<form action={uploadFile}>
<div className="flex flex-col gap-2 m-12 border rounded-md p-4">
<label htmlFor="file">Change Photo</label>
<input type="file" name="file" id="file" />
<button type="submit" id="upload" className="h-8 bg-blue-500 w-28">
Upload file
</button>
</div>
</form>
</>
);
}
import path from "path";
import fs from "fs";
import { revalidatePath } from "next/cache";
const item = {
first: 'Homer',
last: 'Simpson',
email: 'homer.simpson@sfnpp.com',
title: 'Nuclear Safety Inspector',
photo: 'donut.jpg',
};
async function uploadFile(formData: FormData) {
"use server";
const file = formData.get("file") as File;
//console.log("File name:", file.name, "size:", file.size);
if (file.size) {
const fileName = file?.name;
const extension = fileName?.split(".").pop();
let imageName = `${Date.now()}.${extension ?? "jpg"}`;// file.name;
item.photo = imageName;
const imagePath = path.join(`./public/uploads/`, imageName);
const imageStream = fs.createWriteStream(imagePath);
imageStream.write(Buffer.from(await file.arrayBuffer()));
imageStream.end();
}
revalidatePath("/profile");
}
async function getData(barcode: string) {
return item;
}
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div>
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img src={`/uploads/${data?.photo}`} alt={data?.first} className="w-48 " />
</div>
<form action={uploadFile}>
<div className="flex flex-col gap-2 m-12 border rounded-md p-4">
<label htmlFor="file">Change Photo</label>
<input type="file" name="file" id="file" />
<button type="submit" id="upload" className="h-8 bg-blue-500 w-28">
Upload file
</button>
</div>
</form>
</>
);
}
We added a Server Action uploadFile
at the top. This server action receives user submitted formdata and retrieves uploaded file details, and then saves file to uploads
directory.
We also added form
at end of UI to let user select and upload file. Notice that form action refers to uploadFile, so when user submits form, it calls that server action.
Since we are using Server Actions, we need to modify next.config.js
as follows
module.exports = {
experimental: {
serverActions: true,
},
};
module.exports = {
experimental: {
serverActions: true,
},
};
Start project with npm run dev
and go to http://localhost:3000/profile
Click on Choose File
to select a photo and then click Upload file
. This will trigger server action and create file in uploads dir. Note that we are also revalidating path to auto-refresh the UI!
Error Handling
We now have file upload working, so lets add validation and error handling. In order to support validation and error handling, we will need to move our form to a client component. Add a new file called upload-form.tsx
as follows
'use client';
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function UploadForm({
uploadFile,
}: {
uploadFile: (formData: FormData) => Promise<any>,
}) {
const { pending } = useFormStatus();
async function handleFileUpload(formData: FormData) {
try {
//todo: client side data validation, file size, type etc
const data = await uploadFile(formData);
//todo: inform user of success, etc...
console.log(data);
} catch (error) {
//todo: error display
console.log(error);
}
}
return (
<form action={handleFileUpload}>
<fieldset
disabled={pending}
className="flex flex-col gap-2 m-12 border rounded-md p-4"
>
<p>Upload Form</p>
<label htmlFor="file">Change Photo</label>
<input type="file" name="file" id="file" />
<button type="submit" id="upload" className="h-8 bg-blue-500 w-28">
Upload file
</button>
</fieldset>
</form>
);
}
'use client';
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function UploadForm({
uploadFile,
}: {
uploadFile: (formData: FormData) => Promise<any>,
}) {
const { pending } = useFormStatus();
async function handleFileUpload(formData: FormData) {
try {
//todo: client side data validation, file size, type etc
const data = await uploadFile(formData);
//todo: inform user of success, etc...
console.log(data);
} catch (error) {
//todo: error display
console.log(error);
}
}
return (
<form action={handleFileUpload}>
<fieldset
disabled={pending}
className="flex flex-col gap-2 m-12 border rounded-md p-4"
>
<p>Upload Form</p>
<label htmlFor="file">Change Photo</label>
<input type="file" name="file" id="file" />
<button type="submit" id="upload" className="h-8 bg-blue-500 w-28">
Upload file
</button>
</fieldset>
</form>
);
}
handleFileUpload
functions wraps the calls to actual server action in try/catch block and also provide us with place for validation and post processing. We are also using useFormStatus
to disable form when doing upload!
Modify ProfilePage
component in profile.tsx
as follows
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div className="flex flex-col gap-2">
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img
src={`/uploads/${data?.photo}`}
alt={data?.first}
className="w-48 "
/>
</div>
<UploadForm uploadFile={uploadFile} />
</>
);
}
export default async function ProfilePage() {
const data = await getData();
return (
<>
<div className="flex flex-row flex-nowrap p-12 gap-4">
<div className="flex flex-col gap-2">
<h1 className="text-2xl ">{`${data?.first} ${data.last}`}</h1>
<h2 className="text-base ">{data?.email}</h2>
<div className="text-sm ">{data?.title}</div>
</div>
<img
src={`/uploads/${data?.photo}`}
alt={data?.first}
className="w-48 "
/>
</div>
<UploadForm uploadFile={uploadFile} />
</>
);
}
Start project with npm run dev
and go to http://localhost:3000/profile
API Routes
While we explored server actions, same logic also works with API route handlers.
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as File;
console.log("File name:", file.name, "size:", file.size);
}
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as File;
console.log("File name:", file.name, "size:", file.size);
}
Next Steps
Hopefully you now have a good idea of how to upload files using Server Actions. By default, the maximum size of the request body sent to a Server Action is 1MB. However, you can configure this limit using the experimental serverActionsBodySizeLimit
option.
module.exports = {
experimental: {
serverActions: true,
serverActionsBodySizeLimit: '2mb',
},
};
module.exports = {
experimental: {
serverActions: true,
serverActionsBodySizeLimit: '2mb',
},
};
Find Source code with a ready to run project @ GitHub
CreateAppAI
CreateAppAI auto generates Profile route with file upload and supports host of other route patterns.