Upload an image from nextjs to aws S3 bucket using presigned url- Part 2

Upload an image from nextjs to aws S3 bucket using presigned url- Part 2

Introduction

This post is a continuation of the previous post . In the previous post we had seen the architecture of uploading an image to S3 using presigned urls and now lets understand the implementation details and go through the code of a sample nextjs application with the image upload functionality.

Creating a nextjs app

Let us create a new nextjs app with any name of your choice, here we will create the app with the name "nextjs-s3-file-upload" by executing the below command in the terminal inside VSCode.

npx create-next-app@latest

Note that we are using VSCode and npm as the package manager however you can use any code editor or package manager which you are comfortable with.

Installing dependencies

There are few dependencies that we are going to install in order to work with AWS and Forms.

  • @aws-sdk/client-s3 : This package is used to work with AWS from the client app.
  • @aws-sdk/s3-request-presigner : This package is needed to work with the apis needed to generate the presigned urls.
  • react-hook-form : Since we will be using a file control and submit the uploaded file we would need to work with forms and we will be using react hook forms for this.

Configurations in next.config.js

Since we are going to display the image in our app after successful upload we would need the below mentioned configuration in the file next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.amazonaws.com',
      },
    ],
  },
};

module.exports = nextConfig;

Environment Variables (.env.local)

We are going to connect to aws from the nextjs app hence we would need certain details pertaining to aws in order to establish a connection securely.

We are going to declare a few variables for this purpose as mentioned below. Please remember not to commit these values into the repository and make sure your .gitignore file as relevant configuration to excluse this file.

.env.local

S3_ACCESS_KEY="YOUR ACCESS KEY"
S3_SECRET_KEY="YOUR SECRET KEY"
S3_BUCKET_NAME="YOUR AWS BUCKET NAME"
S3_REGION="YOUR S3 REGION"

Now that we have created a new nextjs application, installed required dependencies and configured our environment variables, let us proceed to create an api inside the nextjs app and use that api for making calls to store the image in s3 and get the url to diplay the uploaded image on the screen.

API Route

Create a new api folder inside the app directory and within that create another folder for our api route named presigned-url. Inside the route folder (presigned-url) create a file named route.ts that we contain our route handler methods.

Paste the below code inside the route.ts file and we will go through the code next.

route.ts

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
} from "@aws-sdk/client-s3";
import { NextRequest } from "next/server";

const client = new S3Client({
  region: process.env.S3_REGION,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY,
  },
});

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;

  const file = searchParams.get("file");

  if (!file) {
    return Response.json(
      { error: "File query parameter is required" },
      { status: 400 }
    );
  }

  const putCommand = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME,
    Key: file,
  });

  const getCommand = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME,
    Key: file,
  });

  const url = await getSignedUrl(client, putCommand, { expiresIn: 60 });
  const getUrl = await getSignedUrl(client, getCommand, { expiresIn: 60 });
  return Response.json({ presignedUrl: url, getUrl });
}

As you can see in the first two lines that we are using the packages that we installed initially to get access to methods to help us get the presigned url and establish connectivity with the aws s3.

First we create the s3 client using the environment variables we configured using the below code.

const client = new S3Client({
  region: process.env.S3_REGION,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY,
  },
});

The PutObjectCommand and the GetObjectCommand are used to upload and retrieve the object in our case the image to and from s3.

The getSignedUrl method actually uses the commands and the client with a expiry time as a parameter to produce the presigned url that will be used from the client to upload and dispay image via urls.

We use the getSignedUrl first to upload the image via the PutObjectCommand and store it in a variable url which is passed in the response to the api call. Then the getSignedUrl is used with the GetObjectCommand to retrieve the url to display the image from s3 by sending it in response object to the api call.

The api call we are referring to here is the url api/presigned-ur/ which we just defined in this section.

Creating a client component

Let us now create a components folder withing the app directory that contains all the client components we create in this app.

Now lets create a client component named FileUpload.tsx and the below code into it.

"use client";

import Image from "next/image";
import React, { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";

type FormValues = {
  file: FileList;
};

enum STATUS {
  "SAVING" = "SAVING",
  "SUCCESS" = "SUCCESS",
  "ERROR" = "ERROR",
  "PENDING" = "PENDING",
}

export const FileUpload = () => {
  const [status, setStatus] = useState(STATUS.PENDING);
  const [fileUrl, setFileUrl] = useState("");
  const { register, handleSubmit } = useForm<FormValues>();

  const onSubmit: SubmitHandler<FormValues> = async (data) => {
    if (!data.file[0]) {
      setStatus(STATUS.ERROR);
      return;
    }

    setStatus(STATUS.SAVING);

    const filename = data.file[0].name;

    const res = await fetch(`/api/presigned-url?file=${filename}`);

    const { presignedUrl, getUrl } = (await res.json()) as {
      presignedUrl: string;
      getUrl: string;
    };

    const fileUpload = await fetch(presignedUrl, {
      method: "PUT",
      body: data.file[0],
    });

    if (!fileUpload.ok) {
      setStatus(STATUS.ERROR);
      return;
    }

    setFileUrl(getUrl);
    setStatus(STATUS.SUCCESS);
  };

  return (
    <section>
      <form
        onSubmit={handleSubmit(onSubmit)}
        className="w-full max-w-sm m-auto py-10 mt-10 px-10 border rounded-lg drop-shadow-md bg-white text-gray-600 flex flex-col gap-6"
      >
        <h1 className="text-2xl">Next.js File Upload</h1>
        <p className="text-md">
          STATUS: <span className="font-bold">{status}</span>
        </p>
        <div className="">
          <input
            type="file"
            {...register("file")}
            className="w-full text-gray-600 rounded border-gray-300 focus:ring-gray-500 dark:focus:ring-gray-600 border py-2 px-2"
          />
        </div>
        <div className="">
          <input
            type="submit"
            value="Upload"
            disabled={status === STATUS.SAVING}
            className={`${
              status === STATUS.SAVING ? "cursor-not-allowed" : ""
            } cursor-pointer px-2.5 py-2 font-medium text-gray-900 bg-white rounded-md border border-gray-300 hover:bg-gray-100 hover:text-blue-600  disabled:text-gray-300`}
          />
        </div>

        {fileUrl.length ? (
          <div className="rounded-md overflow-hidden">
            <Image
              src={fileUrl}
              width={350}
              height={350}
              objectFit="cover"
              alt="Uploaded image"
            />
          </div>
        ) : null}
      </form>
    </section>
  );
};

The code above is pretty straightforward. All we are doing is creating a form with a file control. Creating a submit handler for the form and then capturing the file and call the route handler we created earlier to get the url to be used for uploading the image and the url to be used for displaying the image.

We make the first api call to our route handler that gives us the upload and download url for our file.

Next we make use of the upload url which is a presigned url to upload the image. Once the upload is successful we set the download url so that the image can be displayed into our form.

Using the client component

We have completed doing everything needed for the implementation and now we just need to use the component we created above in our nextjs page to complete the application.

Add the below code in our page.tsx to display the component in our app.

import { FileUpload } from './components/FileUpload';

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-100">
      <FileUpload />
    </main>
  );
}

Conclusion

In this post we went through the code that is needed to implement the file upload and download functionality from a nextjs app using presigned url with amazon aws s3 bucket.

This is just one of the approach of achieving the file upload with aws and there are other approaches to upload the file without using the presigned url as well.

Thank you for reading and see you in the next post !👋

Buy a coffee for sudshekhar