NextJs File Upload

In this blog post, we will look at how to upload file in NextJs using Server Actions.

File Upload

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.