ProNextJS

    Next.js File Uploads: Server-Side Solutions

    Jack HerringtonJack Herrington

    I get a lot of questions about forms on NextJS, and a lot of them have to do with posting files to the server. If you've worked in other frameworks you are probably expecting that uploading files is a huge pain, but in NextJS it's actually quite simple. In this article, I'll show you how to upload files to the server using NextJS two different methods.

    Form Actions

    The simplest way to upload a file is using a server action.

    Let's think of a simple image uploading application that takes images and stores them in the /public/uploads directory. We'll start with the server action that is going to receive the uploaded file.

    "use server";
    import fs from "node:fs/promises";
    import { revalidatePath } from "next/cache";
    
    export async function uploadFile(formData: FormData) {
      const file = formData.get("file") as File;
      const arrayBuffer = await file.arrayBuffer();
      const buffer = new Uint8Array(arrayBuffer);
    
      await fs.writeFile(`./public/uploads/${file.name}`, buffer);
    
      revalidatePath("/");
    }
    

    This function takes a FormData object and extracts the file from it. It then writes the file to the /public/uploads directory.

    Most likely, instead of writing the file to the /public/uploads directory, you would want to write it to a cloud storage service like S3. But for the sake of simplicity, we'll just write it to the local filesystem.

    The revalidatePath function is a helper function that tells NextJS to bust the cache on the homepage. This is useful if you want to show all the uploaded file on the homepage. Which is also pretty easy. Here is the code for the homepage:

    import Image from "next/image";
    import fs from "node:fs/promises";
    
    import UploadForm from "./UploadForm";
    
    export default async function Home() {
      const files = await fs.readdir("./public/uploads");
      const images = files
        .filter((file) => file.endsWith(".jpg"))
        .map((file) => `/uploads/${file}`);
    
      return (
        <main>
          <UploadForm />
          <div className="flex flex-wrap">
            {images.map((image) => (
              <div key={image} className="px-2 h-auto w-1/2">
                <Image
                  key={image}
                  src={image}
                  width={400}
                  height={400}
                  alt={image}
                  className="object-cover w-full"
                />
              </div>
            ))}
          </div>
        </main>
      );
    }
    

    This is a React Server Component, so it can use fs to read the files in the /public/uploads directory. It then filters out the files that are not images and in the JSX it uses the NextJS Image component to display the image.

    Again, we are keeping the files locally just for the sake of a simple example that isn't tied to any particular service. You could replace the fs calls with calls to a cloud storage service. Or to a database that would return the images associated with this user.

    The component also brings in the UploadForm component that we'll create next.

    "use client";
    import { uploadFile } from "./upload-action";
    
    export default function UploadForm() {
      return (
        <form action={uploadFile} className="flex flex-col gap-4">
          <label>
            <span>Upload a file</span>
            <input type="file" name="file" ref={fileInput} />
          </label>
          <button type="submit">Submit</button>
        </form>
      );
    }
    

    This is a simple form that takes a file input and a submit button. The action attribute is set to the uploadFile function that we defined earlier. This is a NextJS feature that allows you to call a server action from a form.

    Hitting the "Submit" button triggers the action which is then handled completely by NextJS. If you look at the developer console you can see the POST being automatically generated by NextJS on the client side and then routed through to the server action.

    Honestly, this is the easiest way I've ever seen to upload files in a web application. It's remarkably simple and straightforward.

    However, if you more control over the API endpoint we can create our own API endpoint and fetch to it to upload the file.

    Using An API Route Handler

    The second way to upload files is to use an API route handler. This is a more traditional way to handle file uploads in web applications.

    Show below is the code for the API route handler that will handle the file upload. This code would go into src/app/api/uploadImage/route.ts to create an API route at /api/uploadImage. You can put it wherever you want.

    import { NextResponse } from "next/server";
    import { revalidatePath } from "next/cache";
    import fs from "node:fs/promises";
    
    export async function POST(req: Request) {
      try {
        const formData = await req.formData();
    
        const file = formData.get("file") as File;
        const arrayBuffer = await file.arrayBuffer();
        const buffer = new Uint8Array(arrayBuffer);
        await fs.writeFile(`./public/uploads/${file.name}`, buffer);
    
        revalidatePath("/");
    
        return NextResponse.json({ status: "success" });
      } catch (e) {
        console.error(e);
        return NextResponse.json({ status: "fail", error: e });
      }
    }
    

    As you can see the first part of the code almost identical to the server action we created earlier. The only difference being that we have to wait for the formData from the request object. And then we return a JSON response in addition to calling revalidatePath.

    The client code of the UploadForm becomes a little more complex as we are now managing the fetch call ourselves.

    "use client";
    import { useRef } from "react";
    
    export default function UploadForm() {
      const fileInput = useRef<HTMLInputElement>(null);
    
      async function uploadFile(
        evt: React.MouseEvent<HTMLButtonElement, MouseEvent>
      ) {
        evt.preventDefault();
    
        const formData = new FormData();
        formData.append("file", fileInput?.current?.files?.[0]!);
    
        const response = await fetch("/api/uploadImage", {
          method: "POST",
          body: formData,
        });
        const result = await response.json();
        console.log(result);
      }
    
      return (
        <form className="flex flex-col gap-4">
          <label>
            <span>Upload a file</span>
            <input type="file" name="file" ref={fileInput} />
          </label>
          <button type="submit" onClick={uploadFile}>
            Submit
          </button>
        </form>
      );
    }
    

    Now instead of using an action attribute on the form, we are using an onClick event on the submit button. This event calls the uploadFile function which creates a FormData object and appends the file to it. It then uses fetch to send the file to the API route.

    This is a more traditional way to handle file uploads in web applications. You now have control over where the API endpoint is located as well as the data returned from it. So potentially this endpoint could be used by other applications as well.

    Summing It All Up

    The server actions support ing NextJS App Router makes it incredibly easy to upload files to the server. You can use a server action to handle the file upload directly from the form. But if you prefer to have more control over the mechanism then you can manage the posting yourself with an API endpoint, which is honestly not that much more difficult.

    Subscribe for Free Tips, Tutorials, and Special Discounts

    We're in this together!

    I respect your privacy. Unsubscribe at any time.