<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Daniel Errante's Blog]]></title><description><![CDATA[Full Stack Engineer. Ruby on Rails, Node.js, React.js, Next.js, Terraform, Elixir, Phoenix, AWS]]></description><link>https://blog.danoph.com</link><generator>RSS for Node</generator><lastBuildDate>Tue, 19 May 2026 09:12:10 GMT</lastBuildDate><atom:link href="https://blog.danoph.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Create an Image Tagger App with React.js and AI in Just a Few Simple Steps!]]></title><description><![CDATA[In this tutorial, we'll create a simple application that enables users to upload an image using a React.js form and display image tags using AI. We'll be using Next.js with TailwindCSS since it's straightforward to set up for both the front-end HTML ...]]></description><link>https://blog.danoph.com/create-an-image-tagger-app-with-reactjs-and-ai-in-just-a-few-simple-steps</link><guid isPermaLink="true">https://blog.danoph.com/create-an-image-tagger-app-with-reactjs-and-ai-in-just-a-few-simple-steps</guid><category><![CDATA[AI]]></category><category><![CDATA[image classification]]></category><category><![CDATA[React]]></category><category><![CDATA[Image Annotation]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Daniel Errante]]></dc:creator><pubDate>Wed, 08 Mar 2023 19:26:34 GMT</pubDate><content:encoded><![CDATA[<p>In this tutorial, we'll create a simple application that enables users to upload an image using a React.js form and display image tags using AI. We'll be using Next.js with TailwindCSS since it's straightforward to set up for both the front-end HTML and simple API endpoints with minimal configuration. We'll also be using Amazon's Rekognition API for the AI image tagging.</p>
<p>By the end of this tutorial, you will have an application that looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678300376866/52f0a2b6-6833-4d16-975b-f9a513c807d4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To start with, we need to have an AWS access key and secret available to the local Next.js application. For this tutorial, we assume you already have the credentials. If you need instructions on obtaining those, you can go to this link - <a target="_blank" href="https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html"><strong>https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html</strong></a>. Once you have the credentials, ensure to export <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> in your shell environment locally. If you need help doing that, go to this link - <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html"><strong>https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html</strong></a>.</p>
<h2 id="heading-building-an-image-upload-form">Building an image upload form</h2>
<p>In my previous articles, I showed you how to build a file uploader that can support files up to 5 terabytes directly to Amazon S3 (<a target="_blank" href="https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs">https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs</a>). For this example, supporting 5TB file uploads seems a little overkill so we're going to start from scratch making the simplest thing possible that can accept images up to 5 megabytes. We're not going to store file uploads in a database or deploy the app anywhere. I just want to demonstrate how easy it is to build an image tagger using Amazon Rekognition AI.</p>
<p>Let's start by creating a new Next.js application with TailwindCSS preconfigured:</p>
<pre><code class="lang-bash">npx create-next-app -e with-tailwindcss image-classifier
</code></pre>
<p>After that's done, go into the new app directory and start the local server:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> image-classifier
npm run dev
</code></pre>
<p>When you visit <code>http://localhost:3000</code> your page should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678290342993/a3c7db4c-a668-40fe-9038-38ee91d948fa.png" alt class="image--center mx-auto" /></p>
<p>Now, let's replace the default boilerplate component in <code>pages/index.tsx</code> with our own component that includes a file upload form:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">'next/head'</span>
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">'next/image'</span>

<span class="hljs-keyword">const</span> Home: NextPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];

    <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

      formData.append(<span class="hljs-string">"file"</span>, file);

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/classify-image"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        body: formData
      });

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'response'</span>, response);
    }

    uploadFile();
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex min-h-screen flex-col items-center justify-center py-2"</span>&gt;
      &lt;Head&gt;
        &lt;title&gt;Image Classifier&lt;/title&gt;
        &lt;link rel=<span class="hljs-string">"icon"</span> href=<span class="hljs-string">"/favicon.ico"</span> /&gt;
      &lt;/Head&gt;

      &lt;main className=<span class="hljs-string">"flex w-full flex-1 flex-col items-center justify-center px-20 text-center"</span>&gt;
        &lt;h1 className=<span class="hljs-string">"text-6xl font-bold"</span>&gt;
          Image Labeler
        &lt;/h1&gt;

        &lt;div className=<span class="hljs-string">"mt-6 flex max-w-4xl flex-wrap items-center justify-around sm:w-full"</span>&gt;
          &lt;input
            id=<span class="hljs-string">"file-upload"</span>
            name=<span class="hljs-string">"files"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
            onChange={onFileChanged}
            accept=<span class="hljs-string">"image/*"</span>
          /&gt;
        &lt;/div&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home
</code></pre>
<p>Your page should look like this when you go back to your browser:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678291413831/b6a40bff-9d20-4748-8264-a7f9e1caa500.png" alt class="image--center mx-auto" /></p>
<p>The <code>onChange</code> on the file input calls the <code>onFileChanged</code> function in our component when you select a file and then it uploads the file to the <code>/api/classify-image</code> endpoint which we haven't created yet. Let's do that now.</p>
<h2 id="heading-building-the-image-classifier-endpoint">Building the image classifier endpoint</h2>
<p>We're going to be using the Amazon Rekognition API to label our images and the Formidable library to parse the form submission data from the upload form so we'll need to add two libraries to our project:</p>
<pre><code class="lang-typescript">npm i <span class="hljs-meta">@aws</span>-sdk/client-rekognition formidable
</code></pre>
<p>Adding a new API endpoint is extremely easy in Next.js. All we have to do is name the file what we want the endpoint to be. Since our HTML form sends a POST request to <code>/api/classify-image</code> let's create a new file <code>pages/api/classify-image.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { RekognitionClient, DetectLabelsCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-rekognition"</span>;
<span class="hljs-keyword">import</span> { IncomingForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'formidable'</span>;
<span class="hljs-keyword">import</span> { readFileSync } <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;

<span class="hljs-comment">// we need to disable the default body parser since this endpoint is not accepting JSON</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  api: {
    bodyParser: <span class="hljs-literal">false</span>,
  }
};

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> RekognitionClient({});

<span class="hljs-keyword">const</span> getImageLabels = <span class="hljs-keyword">async</span> (base64EncodedImage: <span class="hljs-built_in">Uint8Array</span>) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> DetectLabelsCommand({
      Image: {
        Bytes: base64EncodedImage
      }
    })
  );

  <span class="hljs-keyword">return</span> response.Labels;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req: NextApiRequest, res: NextApiResponse</span>) </span>{
  <span class="hljs-keyword">const</span> imageBuffer = <span class="hljs-keyword">await</span> (
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      <span class="hljs-keyword">new</span> IncomingForm().parse(req, <span class="hljs-function">(<span class="hljs-params">err, fields, files</span>) =&gt;</span> {
        resolve(readFileSync(files.file.filepath));
      });
    })
  );

  <span class="hljs-keyword">const</span> labels = <span class="hljs-keyword">await</span> getImageLabels(imageBuffer);

  res.status(<span class="hljs-number">200</span>).json(labels);
}
</code></pre>
<p>A few things are going on here:</p>
<ol>
<li><p>Near the top of the file, we are disabling the Next.js <code>bodyParser</code> so it doesn't try and parse the request body as JSON.</p>
</li>
<li><p>The <code>getImageLabels</code> function makes an API request to Amazon <code>Rekognition</code> with our base64 encoded image.</p>
</li>
<li><p>The default exported <code>handler</code> function is the actual <code>Next.js</code> API endpoint that our HTML upload form is calling. We didn't add any form validation or error handling so the function assumes an image file has been uploaded and encoded as <code>multipart/form-data</code>. <code>IncomingForm</code>'s <code>parse</code> function doesn't return a promise (instead it expects a callback function) so we are wrapping that code with a new promise and resolving after the form is done parsing in the callback function. Formidable's <code>IncomingForm</code> parses the multipart form data and then saves the file to a temporary file path on disk by default so that's why we are reading the file from disk. Since <code>readFileSync</code> returns a Node.js <code>Buffer</code> we can pass that straight to the <code>getImageLabels</code> function and get our JSON label array back. After all that we respond to the front-end HTML component with the label data.</p>
</li>
</ol>
<h2 id="heading-changing-the-front-end-to-display-our-image-tags">Changing the front end to display our image tags</h2>
<p>Right now we aren't doing anything with the API response after we upload the file to our backend API endpoint besides logging to the browser console. Let's fix that by adding some state to keep track of the image labels that come back from the server.</p>
<p>First, import <code>useState</code> at the top of the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
</code></pre>
<p>And then let's add the state variable inside our component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [imageLabels, setImageLabels] = useState([]);
</code></pre>
<p>Now, we can use <code>setImageLabels</code> to capture the API response from the server inside our <code>onFileChanged</code> function. We can also remove the <code>console.log</code> that was there before so our function looks like this now:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];

    <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

      formData.append(<span class="hljs-string">"file"</span>, file);

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/classify-image"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        body: formData
      });

      setImageLabels(<span class="hljs-keyword">await</span> response.json());
    }

    uploadFile();
  };
</code></pre>
<p>In the HTML view, let's display each label along with the confidence score that Rekognition gives us:</p>
<pre><code class="lang-typescript">        {imageLabels.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
          &lt;ul role=<span class="hljs-string">"list"</span> className=<span class="hljs-string">"mt-6 max-w-md sm:w-full"</span>&gt;
            {imageLabels.map(<span class="hljs-function">(<span class="hljs-params">label, index</span>) =&gt;</span> (
              &lt;li key={index} className=<span class="hljs-string">"py-2 flex items-center justify-between"</span>&gt;
                &lt;div&gt;
                  {label.Name}
                &lt;/div&gt;
                &lt;div className=<span class="hljs-string">"text-lg font-bold"</span>&gt;
                  {<span class="hljs-built_in">Math</span>.round(label.Confidence)}%
                &lt;/div&gt;
              &lt;/li&gt;
            ))}
          &lt;/ul&gt;
        )}
</code></pre>
<p>We are rounding the <code>Confidence</code> score is just for a nicer display since they are 8 or 9 decimal points.</p>
<p>It'd also be nice if we could see the image along with the labels that Rekognition returned. To keep things simple, let's add a preview image in the client-side HTML so we don't have to add any server-side code:</p>
<pre><code class="lang-typescript">        {previewUrl &amp;&amp; (
          &lt;div className=<span class="hljs-string">"mt-6 h-96 aspect-w-10 aspect-h-7 block w-full overflow-hidden relative"</span>&gt;
            &lt;Image
              alt=<span class="hljs-string">"file uploader preview"</span>
              src={previewUrl}
              fill
              sizes=<span class="hljs-string">"(max-width: 768px) 100vw,
              (max-width: 1200px) 50vw,
              33vw"</span>
              quality={<span class="hljs-number">100</span>}
              className=<span class="hljs-string">"pointer-events-none object-contain"</span>
            /&gt;
          &lt;/div&gt;
        )}
</code></pre>
<p>Since we haven't declared <code>previewUrl</code> yet, let's add that to our state at the top of the component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [previewUrl, setPreviewUrl] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
</code></pre>
<p>And let's set the <code>previewUrl</code> after an image file has been selected in our <code>onFileChanged</code> function:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];

    setPreviewUrl(URL.createObjectURL(file));

    <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

      formData.append(<span class="hljs-string">"file"</span>, file);

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/classify-image"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        body: formData
      });

      setImageLabels(<span class="hljs-keyword">await</span> response.json());
    }

    uploadFile();
  };
</code></pre>
<p><strong>Side Note</strong> - If you've ever wanted to display images client side before they get uploaded to a server, this technique isn't dependent on React or Next.js.</p>
<p>So, after all of those changes, our <code>pages/index.tsx</code> file looks like this now:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">'next/head'</span>
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">'next/image'</span>

<span class="hljs-keyword">const</span> Home: NextPage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [previewUrl, setPreviewUrl] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [imageLabels, setImageLabels] = useState([]);

  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = event.target.files[<span class="hljs-number">0</span>];

    setPreviewUrl(URL.createObjectURL(file));

    <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

      formData.append(<span class="hljs-string">"file"</span>, file);

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/api/classify-image"</span>, {
        method: <span class="hljs-string">"POST"</span>,
        body: formData
      });

      setImageLabels(<span class="hljs-keyword">await</span> response.json());
    }

    uploadFile();
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex min-h-screen flex-col items-center py-2"</span>&gt;
      &lt;Head&gt;
        &lt;title&gt;Image Tagger&lt;/title&gt;
        &lt;link rel=<span class="hljs-string">"icon"</span> href=<span class="hljs-string">"/favicon.ico"</span> /&gt;
      &lt;/Head&gt;

      &lt;main className=<span class="hljs-string">"flex w-full flex-1 flex-col items-center px-20"</span>&gt;
        &lt;h1 className=<span class="hljs-string">"text-6xl font-bold text-center"</span>&gt;
          Image Tagger
        &lt;/h1&gt;

        &lt;div className=<span class="hljs-string">"mt-6 flex max-w-4xl flex-wrap items-center justify-around sm:w-full"</span>&gt;
          &lt;input
            id=<span class="hljs-string">"file-upload"</span>
            name=<span class="hljs-string">"files"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
            onChange={onFileChanged}
            accept=<span class="hljs-string">"image/*"</span>
          /&gt;
        &lt;/div&gt;

        {previewUrl &amp;&amp; (
          &lt;div className=<span class="hljs-string">"mt-6 h-96 aspect-w-10 aspect-h-7 block w-full overflow-hidden relative"</span>&gt;
            &lt;Image
              alt=<span class="hljs-string">"file uploader preview"</span>
              src={previewUrl}
              fill
              sizes=<span class="hljs-string">"(max-width: 768px) 100vw,
              (max-width: 1200px) 50vw,
              33vw"</span>
              quality={<span class="hljs-number">100</span>}
              className=<span class="hljs-string">"pointer-events-none object-contain"</span>
            /&gt;
          &lt;/div&gt;
        )}

        {imageLabels.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
          &lt;ul role=<span class="hljs-string">"list"</span> className=<span class="hljs-string">"mt-6 max-w-md sm:w-full"</span>&gt;
            {imageLabels.map(<span class="hljs-function">(<span class="hljs-params">label, index</span>) =&gt;</span> (
              &lt;li key={index} className=<span class="hljs-string">"py-2 flex items-center justify-between"</span>&gt;
                &lt;div&gt;
                  {label.Name}
                &lt;/div&gt;
                &lt;div className=<span class="hljs-string">"text-lg font-bold"</span>&gt;
                  {<span class="hljs-built_in">Math</span>.round(label.Confidence)}%
                &lt;/div&gt;
              &lt;/li&gt;
            ))}
          &lt;/ul&gt;
        )}
      &lt;/main&gt;
    &lt;/div&gt;
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home
</code></pre>
<p>In the browser, click on <code>Choose File</code> and select an image file under 5 megabytes (that is the limit for Amazon Rekognition - <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-rekognition/interfaces/image.html">https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-rekognition/interfaces/image.html</a>). You might see a slight delay depending on the size of the image you uploaded, but after the file gets sent to Amazon Rekgonition and gets processed, you will see tags showing up for your uploaded image!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678299102173/8bb08ec1-b303-4874-934e-64e4061f6d6d.png" alt class="image--center mx-auto" /></p>
<p>The full source code for this example is available here: <a target="_blank" href="https://github.com/danoph/ai-image-tagger">https://github.com/danoph/ai-image-tagger</a></p>
]]></content:encoded></item><item><title><![CDATA[How To: Next.js Drag-and-Drop Image Uploads Directly to S3 and Displaying with CloudFront]]></title><description><![CDATA[In my first two tutorials on Next.js file uploads, we learned how to build a multiple file upload form in Next.js with drag-and-drop support and progress bars. Our files get stored in Amazon S3 but we aren't displaying the files anywhere on our page ...]]></description><link>https://blog.danoph.com/how-to-nextjs-drag-and-drop-image-uploads-directly-to-s3-and-displaying-with-cloudfront</link><guid isPermaLink="true">https://blog.danoph.com/how-to-nextjs-drag-and-drop-image-uploads-directly-to-s3-and-displaying-with-cloudfront</guid><category><![CDATA[Amazon S3]]></category><category><![CDATA[cloudfront]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Drag & Drop]]></category><category><![CDATA[File Upload]]></category><dc:creator><![CDATA[Daniel Errante]]></dc:creator><pubDate>Mon, 06 Mar 2023 18:01:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678116733502/e65e5eaa-addf-4b9b-9a9d-e63d45393716.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678125475396/55d3a4af-b849-4d5d-9069-22d3cdc8ca6f.gif" alt class="image--center mx-auto" /></p>
<p>In my first two tutorials on Next.js file uploads, we learned how to build a multiple file upload form in Next.js with drag-and-drop support and progress bars. Our files get stored in Amazon S3 but we aren't displaying the files anywhere on our page after the uploads are complete. In this tutorial, I'm going to change our multiple file uploader to only allow images to be uploaded and then we're going to display those images on our page using CloudFront and Amazon S3.</p>
<p>If you'd like to see the two previous articles, here they are:</p>
<ol>
<li><p><a target="_blank" href="https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs">https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs</a></p>
</li>
<li><p><a target="_blank" href="https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support">https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support</a></p>
</li>
</ol>
<p>And if you'd just like to see the source code for this tutorial, go here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo/tree/story/cloudfront">https://github.com/danoph/file-uploader-demo/tree/story/cloudfront</a></p>
<p>By the end of this article, your application will look like this and display images from your S3 bucket we set up previously:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678116931255/c7d853ee-78df-4074-86a8-1cd2e4da94fd.png" alt class="image--center mx-auto" /></p>
<p>We're going to start from the point we left off in the previous article, <a target="_blank" href="https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support">https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support</a>.</p>
<h2 id="heading-changing-our-file-uploader-to-only-allow-images">Changing our file uploader to only allow images</h2>
<p>Ok, let's start by modifying our form's file input to only allow image uploads:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

&lt;input
  id=<span class="hljs-string">"file-upload"</span>
  name=<span class="hljs-string">"files"</span>
  <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
  className=<span class="hljs-string">"sr-only"</span>
  onChange={onFilesChanged}
  multiple
  accept=<span class="hljs-string">"image/*"</span>
  value={inputValue}
/&gt;
</code></pre>
<p>We should also check the mime type of the filename for our API endpoint in <code>pages/api/multipart_uploads/index.ts</code> and we'll introduce a third-party library called <code>mime-types</code> to help us out:</p>
<pre><code class="lang-bash">npm install mime-types
</code></pre>
<p>In <code>pages/api/multipart_uploads/index.ts</code> let's change our function to return an error if the upload is not an image:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/multipart_uploads/index.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { createMultipartUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;
<span class="hljs-keyword">import</span> mime <span class="hljs-keyword">from</span> <span class="hljs-string">'mime-types'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-keyword">const</span> { filename } = req.body;

  <span class="hljs-keyword">if</span> (mime.lookup(filename).match(<span class="hljs-regexp">/image\//</span>)) {
    <span class="hljs-keyword">const</span> { uploadId, fileKey } = <span class="hljs-keyword">await</span> createMultipartUpload({ filename });

    res.status(<span class="hljs-number">201</span>).json({
      uploadId,
      fileKey,
    });
  } <span class="hljs-keyword">else</span> {
    res.status(<span class="hljs-number">422</span>).json({
      message: <span class="hljs-string">"Upload must be an image"</span>
    });
  }
}
</code></pre>
<h2 id="heading-setting-up-our-cloudfront-cdn">Setting up our CloudFront CDN</h2>
<p>To display our images, we need to serve them over the web through a publicly available URL. I'm not going to assume you have a custom domain name to work with, so we can just use the default URL when we create our CloudFront distribution. If you'd like to see me walk you through how to use a custom domain name with a CloudFront distribution leave a comment and I'll write an article on how to do that.</p>
<p>Since we already have Terraform set up which I walked you through in the first article (<a target="_blank" href="https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs">https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs</a>) new infrastructure changes are going to be a lot easier than using the AWS Console. If you haven't already set up Terraform and run through the first article, go do that now.</p>
<p>We already have our Amazon S3 bucket set up in Terraform, so now we need to add a CloudFront distribution to serve files from that S3 bucket:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># main.tf</span>

resource <span class="hljs-string">"aws_s3_bucket_public_access_block"</span> <span class="hljs-string">"uploads"</span> {
  bucket = aws_s3_bucket.uploads.id

  block_public_acls       = <span class="hljs-literal">true</span>
  block_public_policy     = <span class="hljs-literal">true</span>
  ignore_public_acls      = <span class="hljs-literal">true</span>
  restrict_public_buckets = <span class="hljs-literal">true</span>
}

resource <span class="hljs-string">"aws_cloudfront_distribution"</span> <span class="hljs-string">"uploads"</span> {
  origin {
    domain_name              = aws_s3_bucket.uploads.bucket_regional_domain_name
    origin_id                = <span class="hljs-string">"file-uploader-demo"</span>

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.uploads.cloudfront_access_identity_path
    }
  }

  enabled             = <span class="hljs-literal">true</span>

  default_cache_behavior {
    allowed_methods  = [<span class="hljs-string">"DELETE"</span>, <span class="hljs-string">"GET"</span>, <span class="hljs-string">"HEAD"</span>, <span class="hljs-string">"OPTIONS"</span>, <span class="hljs-string">"PATCH"</span>, <span class="hljs-string">"POST"</span>, <span class="hljs-string">"PUT"</span>]
    cached_methods   = [<span class="hljs-string">"GET"</span>, <span class="hljs-string">"HEAD"</span>]
    target_origin_id = <span class="hljs-string">"file-uploader-demo"</span>

    forwarded_values {
      query_string = <span class="hljs-literal">false</span>

      cookies {
        forward = <span class="hljs-string">"none"</span>
      }
    }

    viewer_protocol_policy = <span class="hljs-string">"allow-all"</span>
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  viewer_certificate {
    cloudfront_default_certificate = <span class="hljs-literal">true</span>
  }

  restrictions {
    geo_restriction {
      restriction_type = <span class="hljs-string">"none"</span>
    }
  }
}

resource <span class="hljs-string">"aws_cloudfront_origin_access_identity"</span> <span class="hljs-string">"uploads"</span> {}

data <span class="hljs-string">"aws_iam_policy_document"</span> <span class="hljs-string">"cloudfront_s3"</span> {
  statement {
    actions   = [<span class="hljs-string">"s3:GetObject"</span>]
    resources = [<span class="hljs-string">"<span class="hljs-variable">${aws_s3_bucket.uploads.arn}</span>/*"</span>]

    principals {
      <span class="hljs-built_in">type</span>        = <span class="hljs-string">"AWS"</span>
      identifiers = [aws_cloudfront_origin_access_identity.uploads.iam_arn]
    }
  }
}

resource <span class="hljs-string">"aws_s3_bucket_policy"</span> <span class="hljs-string">"uploads"</span> {
  bucket = aws_s3_bucket.uploads.id
  policy = data.aws_iam_policy_document.cloudfront_s3.json
}

output <span class="hljs-string">"cloudfront_url"</span> {
  value = aws_cloudfront_distribution.uploads.domain_name
}
</code></pre>
<p>Run <code>terraform apply</code> and note the <code>cloudfront_url</code> output that Terraform prints. We will need this when displaying our images and we also need to put this value in our <code>next.config.js</code> file to allow images coming from that domain:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/** @type {import('next').NextConfig} */</span>
<span class="hljs-keyword">const</span> nextConfig = {
  reactStrictMode: <span class="hljs-literal">true</span>,
  images: {
    remotePatterns: [
      {
        protocol: <span class="hljs-string">'https'</span>,
        hostname: <span class="hljs-string">'d1m5oohnuppcs9.cloudfront.net'</span>, <span class="hljs-comment">// replace this with your cloudfront_url</span>
      },
    ],
  },
}

<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = nextConfig
</code></pre>
<p>Since we changed the <code>next.config.js</code> file we will need to restart the server before being able to use Next.js <code>Image</code> in our code.</p>
<h2 id="heading-creating-an-image-gallery">Creating an Image Gallery</h2>
<p>Since we're using Tailwind CSS we can create a decent-looking image gallery without a ton of code and we get image optimization for free out of the box with Next.js. Let's add a new <code>ImageGallery</code> component to <code>pages/index.tsx</code> . We'll also need to add an import to the top of the file for <code>next/image</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">'next/image'</span>;

<span class="hljs-keyword">const</span> ImageGallery = <span class="hljs-function">(<span class="hljs-params">{ images }</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    &lt;ul role=<span class="hljs-string">"list"</span> className=<span class="hljs-string">"py-4 grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8"</span>&gt;
      {images.map(<span class="hljs-function">(<span class="hljs-params">image</span>) =&gt;</span> (
        &lt;li key={image.source} className=<span class="hljs-string">"relative"</span>&gt;
          &lt;div className=<span class="hljs-string">"group h-48 aspect-w-10 aspect-h-7 block w-full overflow-hidden rounded-lg bg-gray-100 focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 focus-within:ring-offset-gray-100 relative"</span>&gt;
            &lt;Image
              src={image.source}
              fill
              sizes=<span class="hljs-string">"(max-width: 768px) 100vw,
              (max-width: 1200px) 50vw,
              33vw"</span>
              alt=<span class="hljs-string">""</span>
              quality={<span class="hljs-number">100</span>}
              className=<span class="hljs-string">"pointer-events-none object-cover group-hover:opacity-75"</span>
            /&gt;
            &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span> className=<span class="hljs-string">"absolute inset-0 focus:outline-none"</span>&gt;
              &lt;span className=<span class="hljs-string">"sr-only"</span>&gt;View details <span class="hljs-keyword">for</span> {image.title}&lt;/span&gt;
            &lt;/button&gt;
          &lt;/div&gt;
          &lt;p className=<span class="hljs-string">"pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900"</span>&gt;{image.title}&lt;/p&gt;
          &lt;p className=<span class="hljs-string">"pointer-events-none block text-sm font-medium text-gray-500"</span>&gt;
            { prettyBytes(image.size) }
          &lt;/p&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}
</code></pre>
<p>Next let's use our new <code>ImageGallery</code> component inside of our <code>HomeComponent</code> and add some state to keep track of <code>images</code> that we'll need to pass into the <code>ImageGallery</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [images, setImages] = useState([]);
</code></pre>
<p>And add the <code>ImageGallery</code> to the inside of <code>HomeComponent</code>'s view:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;ImageGallery images={images} /&gt;
</code></pre>
<p>So your full <code>HomeComponent</code> should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [uploads, setUploads] = useState&lt;FileUpload[]&gt;([]);
  <span class="hljs-keyword">const</span> [draggingOver, setDraggingOver] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> [images, setImages] = useState([]);

  <span class="hljs-keyword">const</span> updateProgress = <span class="hljs-function">(<span class="hljs-params">filename, percentage</span>) =&gt;</span> {
    setUploads(<span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span>
      state.map(<span class="hljs-function"><span class="hljs-params">fileUpload</span> =&gt;</span>
        fileUpload.uploader.file.name === filename
        ? { ...fileUpload, progress: percentage }
        : fileUpload
      )
    );
  };

  <span class="hljs-keyword">const</span> addFiles = <span class="hljs-function"><span class="hljs-params">files</span> =&gt;</span> {
    setUploads(
      files.map(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> ({
        uploader: <span class="hljs-keyword">new</span> Uploader({ file })
        .onProgress(<span class="hljs-function">(<span class="hljs-params">{ percentage }</span>) =&gt;</span> {
          updateProgress(file.name, percentage);
        })
        .onComplete(<span class="hljs-function">(<span class="hljs-params">uploadResponse</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload complete'</span>, uploadResponse);
        })
        .onError(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'upload error'</span>, error)
        }),
        progress: <span class="hljs-number">0</span>
      }))
    );
  };

  <span class="hljs-keyword">const</span> onFilesChanged = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> files = [ ...e.target.files ];
    addFiles(files);
  };

  <span class="hljs-keyword">const</span> uploadClicked = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!uploads.length) { <span class="hljs-keyword">return</span> }

    uploads.forEach(<span class="hljs-function"><span class="hljs-params">upload</span> =&gt;</span> upload.uploader.start());
  };

  <span class="hljs-keyword">const</span> stopEvent = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    e.preventDefault();
    e.stopPropagation();
  }

  <span class="hljs-keyword">const</span> handleDragEnter = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
  };

  <span class="hljs-keyword">const</span> handleDragLeave = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">const</span> handleDragOver = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">true</span>);
  };

  <span class="hljs-keyword">const</span> handleDrop = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> files = [ ...e.dataTransfer.files ];
    addFiles(files);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;ImageGallery images={images} /&gt;

      &lt;div className=<span class="hljs-string">"flex text-sm text-gray-600"</span>&gt;
        &lt;div className=<span class="hljs-string">"w-full"</span>&gt;
          &lt;div
            className={<span class="hljs-string">`<span class="hljs-subst">${draggingOver
              ? <span class="hljs-string">"border-blue-500"</span>
              : <span class="hljs-string">"border-gray-300"</span>
            }</span> mt-1 flex items-center justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md`</span>}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            onDragEnter={handleDragEnter}
            onDragLeave={handleDragLeave}
          &gt;
            &lt;label
              htmlFor=<span class="hljs-string">"file-upload"</span>
              className=<span class="hljs-string">"inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
            &gt;
              &lt;div className=<span class="hljs-string">"text-md"</span>&gt;
                Choose File
              &lt;/div&gt;
              &lt;input
                id=<span class="hljs-string">"file-upload"</span>
                name=<span class="hljs-string">"files"</span>
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
                className=<span class="hljs-string">"sr-only"</span>
                onChange={onFilesChanged}
                multiple
                accept=<span class="hljs-string">"image/*"</span>
                value={inputValue}
              /&gt;
            &lt;/label&gt;
            &lt;p className=<span class="hljs-string">"pl-1 text-sm"</span>&gt;or drag and drop&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;p className=<span class="hljs-string">"py-2 text-sm text-gray-500"</span>&gt;
        Any file up to <span class="hljs-number">5</span>TB
      &lt;/p&gt;

      {uploads.map(<span class="hljs-function">(<span class="hljs-params">{ uploader: { file }, progress }</span>) =&gt;</span> (
        &lt;div key={file.name} className=<span class="hljs-string">"py-2 flex flex-grow flex-col"</span>&gt;
          &lt;span className=<span class="hljs-string">"text-sm font-medium text-gray-900"</span>&gt;
            { file.name }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { file.type }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { prettyBytes(file.size) }
          &lt;/span&gt;

          &lt;UploadProgressBar
            progress={progress}
          /&gt;
        &lt;/div&gt;
      ))}

      &lt;button
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span>
        className=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        onClick={uploadClicked}
      &gt;
        Upload File
      &lt;/button&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>If you haven't read my previous articles and are wondering what all the code is related to uploads, check out those articles first that I linked to at the top of the article.</p>
<h2 id="heading-adding-an-api-endpoint-to-fetch-uploaded-images">Adding an API endpoint to fetch uploaded images</h2>
<p>You might be wondering where our images are going to come from. Since we don't have a database yet, we can build a simple API endpoint to query files in our S3 bucket and return the images to the front end along with some metadata about the image like its filename, size, and CloudFront URL. Let's create <code>pages/api/images.ts</code> and add a handler function:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/images.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { listBucketFiles } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;
<span class="hljs-keyword">import</span> mime <span class="hljs-keyword">from</span> <span class="hljs-string">'mime-types'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> UploadedImage {
  title: <span class="hljs-built_in">string</span>;
  source: <span class="hljs-built_in">string</span>;
  size: <span class="hljs-built_in">number</span>;
}

<span class="hljs-comment">// the S3 Object field types are defined as string | null so we have to be a little defensive when using</span>
<span class="hljs-comment">// those objects in our TypeScript code</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res: NextApiResponse&lt;UploadedImage[]&gt;</span>) </span>{
  <span class="hljs-keyword">const</span> images = (<span class="hljs-keyword">await</span> listBucketFiles())
  .filter(<span class="hljs-function"><span class="hljs-params">s3File</span> =&gt;</span> mime.lookup(s3File?.Key || <span class="hljs-string">""</span>).match(<span class="hljs-regexp">/image\//</span>))
  .map(<span class="hljs-function"><span class="hljs-params">s3File</span> =&gt;</span> ({
    title: s3File.Key || <span class="hljs-string">"some image"</span>,
    source: <span class="hljs-string">`https://d1m5oohnuppcs9.cloudfront.net/<span class="hljs-subst">${s3File.Key}</span>`</span>,
    size: s3File.Size || <span class="hljs-number">0</span>,
  }));

  res.status(<span class="hljs-number">200</span>).json(images);
}
</code></pre>
<p>You'll need to replace <code>d1m5oohnuppcs9.cloudfront.net</code> with your <code>cloudfront_url</code> from the Terraform output from earlier.</p>
<p>Some things to note:</p>
<ul>
<li><p>I created a new TypeScript interface <code>UploadedImage</code> for what we expect in the front-end code to populate the <code>ImageGallery</code> component.</p>
</li>
<li><p>The Amazon javascript sdk v3 is a little cumbersome to work with because all the values for <code>s3File</code> can be undefined. That's why I added defaults to the <code>title</code> and <code>size</code> properties so TypeScript doesn't complain about <code>s3File.Key</code> and <code>s3File.Size</code> possibly being undefined.</p>
</li>
<li><p>I added a <code>filter</code> to the array of objects coming back from S3 to skip files that aren't images.</p>
</li>
</ul>
<p>You'll notice that we are referencing a function called <code>listBucketFiles</code> that we haven't defined yet, so let's fix that now in the <code>lib/s3.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/s3.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> listBucketFiles = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> ListObjectsCommand({
      Bucket: UPLOAD_BUCKET,
    })
  );

  <span class="hljs-keyword">return</span> response?.Contents || [];
}
</code></pre>
<p>We'll also need to add <code>ListObjectsCommand</code> to the list of imports at the top of <code>lib/s3.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/s3.ts</span>

<span class="hljs-keyword">import</span> {
    S3Client,
    CreateMultipartUploadCommand,
    UploadPartCommand,
    CompleteMultipartUploadCommand,
    ListObjectsCommand,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-s3"</span>;
</code></pre>
<h2 id="heading-fetching-the-images-from-our-new-api-endpoint">Fetching the images from our new API Endpoint</h2>
<p>Now that we have an API endpoint set up to fetch images from s3 and transform each s3 object into an <code>UploadedImage</code> we can modify our <code>HomeComponent</code> to fetch images from the API. Let's add <code>axios</code> to the top of <code>pages/index.tsx</code> first:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>

<span class="hljs-keyword">const</span> API_BASE_URL = <span class="hljs-string">"/api/"</span>;

<span class="hljs-keyword">const</span> api = axios.create({
  baseURL: API_BASE_URL,
});
</code></pre>
<p>And then add a <code>useEffect</code> in our <code>HomeComponent</code> to fetch the images from our API endpoint:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> fetchImages = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> api.request({
        url: <span class="hljs-string">`/images`</span>,
        method: <span class="hljs-string">"GET"</span>,
      })

      setImages(response.data);
    };

    fetchImages();
  }, []);
</code></pre>
<p>When you refresh the page you should now see images being displayed in the image gallery we just built:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678117224081/07e15fe3-68a8-40c6-9b49-71cfbfa214b9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-displaying-images-as-they-finish-uploading">Displaying images as they finish uploading</h2>
<p>To display the images immediately after they're uploaded without refreshing the page, we'll need to modify our <code>HomeComponent</code> and the <code>/api/multipart_uploads/[uploadId]/completions</code> endpoint.</p>
<p>Let's start with the front end <code>HomeComponent</code>. After a file is done uploading, we want to take the response from the endpoint when an upload is finished and add that to our <code>images</code> array. Inside of <code>addFiles</code> where we are setting up our file uploads, change the <code>onComplete</code> function to this:</p>
<pre><code class="lang-typescript">        .onComplete(<span class="hljs-function">(<span class="hljs-params">newImage</span>) =&gt;</span> {
          setImages(<span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span> [ newImage, ...state ]);
        })
</code></pre>
<p>TypeScript is going to complain that we didn't set a type for our <code>images</code> state so let's add a type to the array:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [images, setImages] = useState&lt;UploadedImage[]&gt;([]);
</code></pre>
<p>We defined the <code>UploadedImage</code> interface in our <code>pages/api/images.ts</code> file but we are going to need it in another endpoint so let's create a new file <code>lib/models.ts</code> that can export this interface:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/models.ts</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> UploadedImage {
  title: <span class="hljs-built_in">string</span>;
  source: <span class="hljs-built_in">string</span>;
  size: <span class="hljs-built_in">number</span>;
}
</code></pre>
<p>Then let's import <code>UploadedImage</code> so we can use it in our <code>HomeComponent</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">import</span> { UploadedImage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/models'</span>;
</code></pre>
<p>Since we don't want to have this interface defined twice, remove the interface from <code>pages/api/images.ts</code> and add the same import statement to the top:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/images.ts</span>

<span class="hljs-keyword">import</span> { UploadedImage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/models'</span>;
</code></pre>
<p>Now let's modify our endpoint that gets called when an upload is finished to complete the multipart upload so it returns an <code>UploadedImage</code> to the Next.js component:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/multipart_uploads/[uploadId]/completions.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { getS3ObjectMetadata, finishMultipartUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;
<span class="hljs-keyword">import</span> { UploadedImage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/models'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res: NextApiResponse&lt;UploadedImage&gt;</span>) </span>{
  <span class="hljs-keyword">const</span> { uploadId } = req.query;
  <span class="hljs-keyword">const</span> { fileKey, parts } = req.body;

  <span class="hljs-keyword">const</span> finishResponse = <span class="hljs-keyword">await</span> finishMultipartUpload({
    fileKey,
    uploadId,
    parts
  });

  <span class="hljs-keyword">const</span> objectMetadata = <span class="hljs-keyword">await</span> getS3ObjectMetadata({ filename: fileKey });

  res.status(<span class="hljs-number">200</span>).json({
    title: finishResponse.Key || <span class="hljs-string">""</span>,
    source: <span class="hljs-string">`https://d1m5oohnuppcs9.cloudfront.net/<span class="hljs-subst">${finishResponse.Key}</span>`</span>,
    size: objectMetadata.ContentLength || <span class="hljs-number">0</span>,
  });
}
</code></pre>
<p>Some things to note:</p>
<ul>
<li><p>There is one minor problem with the <code>CompleteMultipartUploadCommand</code> because it does not return the size of the file as part of the response. That is why I am adding a new <code>getS3ObjectMetadata</code> function to <code>lib/s3.ts</code> to get the file size that I will show you in a minute.</p>
</li>
<li><p>The endpoint returns the same <code>UploadedImage</code> as the <code>pages/api/images.ts</code> endpoint.</p>
</li>
<li><p>You'll need to replace <code>d1m5oohnuppcs9.cloudfront.net</code> above with your <code>cloudfront_url</code> that we got a few steps earlier.</p>
</li>
</ul>
<p>Now, let's add <code>getS3ObjectMetadata</code> to <code>lib/s3.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getS3ObjectMetadata = <span class="hljs-keyword">async</span> ({ filename }) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> HeadObjectCommand({
      Bucket: UPLOAD_BUCKET,
      Key: filename,
    })
  );

  <span class="hljs-keyword">return</span> response;
}
</code></pre>
<p>We'll also need to import <code>HeadObjectCommand</code> so let's add that to the functions we are importing from <code>@aws-sdk/client-s3</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    S3Client,
    CreateMultipartUploadCommand,
    UploadPartCommand,
    CompleteMultipartUploadCommand,
    ListObjectsCommand,
    HeadObjectCommand,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-s3"</span>;
</code></pre>
<p>Now when you upload images they will each get added as they are finished as the first item in the image gallery:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678122194269/3aa3475a-a487-4cb1-a793-daa2625cb8ac.png" alt class="image--center mx-auto" /></p>
<p>Let me know if have any questions in the comments!</p>
<p>If you want to see the full source code for this tutorial go here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo/tree/story/cloudfront">https://github.com/danoph/file-uploader-demo/tree/story/cloudfront</a></p>
]]></content:encoded></item><item><title><![CDATA[Master React File Uploading: Convert Single File  Uploads into Multi File Uploads with Progress Bars and Drag-and-Drop Support in Just a Few Steps]]></title><description><![CDATA[This is a continuation of the previous article: How to Upload Large Files Directly to Amazon S3 in React/Next.js
Our "finished" React s3 direct uploader that we built has some limitations. We can only upload 1 file at a time, we don't have any progre...]]></description><link>https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support</link><guid isPermaLink="true">https://blog.danoph.com/turning-our-react-single-file-s3-direct-uploader-into-a-multi-file-uploader-adding-progress-bars-and-drag-and-drop-support</guid><category><![CDATA[React]]></category><category><![CDATA[AWS]]></category><category><![CDATA[progress bar]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Daniel Errante]]></dc:creator><pubDate>Tue, 28 Feb 2023 19:30:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/BR6lrzCPYPk/upload/14269a67b4a0becad56fc42f53aadd26.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a continuation of the previous article: <a target="_blank" href="https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs">How to Upload Large Files Directly to Amazon S3 in React/Next.js</a></p>
<p>Our "finished" React s3 direct uploader that we built has some limitations. We can only upload 1 file at a time, we don't have any progress indicators, and we would like to support dragging and dropping files into a drop area with some visual indicators. In this article, we will give the front end a little TLC with Tailwind CSS and expand our uploader to be able to handle many uploads at the same time.</p>
<p>Currently, our application looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677521775483/84db9849-1f1e-44c0-9386-593be12545d7.png" alt class="image--center mx-auto" /></p>
<p>By the end of this article, your application will look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677611414797/73c6bbd7-6970-4490-85dd-03812a6de187.png" alt class="image--center mx-auto" /></p>
<p>tldr; The full source code for this example is here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload">https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload</a></p>
<p>I think the simplest thing to add first would be to add a little bit of styling as well as a progress bar for the single file uploader form. Let's add a progress bar component to our <code>pages/index.tsx</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">const</span> UploadProgressBar = <span class="hljs-function">(<span class="hljs-params">{ progress }</span>) =&gt;</span> (
  &lt;div className=<span class="hljs-string">"py-5"</span>&gt;
    &lt;div className=<span class="hljs-string">"w-full bg-gray-200 rounded-full"</span>&gt;
      &lt;div
        className={<span class="hljs-string">`<span class="hljs-subst">${
          progress === <span class="hljs-number">0</span> ?
            <span class="hljs-string">'invisible'</span>
            : <span class="hljs-string">''</span>
        }</span> bg-indigo-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full`</span>}
        style={{ width: progress + <span class="hljs-string">"%"</span> }}&gt;
        {progress}%
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
);
</code></pre>
<p>Instead of immediately starting an upload when we choose a file, let's add an explicit button to start the upload and give a little less weight to our current upload button since it will be used to stage the file for upload. We can also add a few more details about the file selected like the file name, type and size. I want to show a human friendly size, so we can introduce a third party library called <code>pretty-bytes</code> that will convert bytes to KB, MB GB, etc:</p>
<pre><code class="lang-bash">npm i pretty-bytes
</code></pre>
<p>And reference it in our Home component. We will also need to add a useState to keep track of the file upload progress. We also need to modify the <code>uploader.onProgress</code> method so we can update progress as needed. Here is the modified Home component that uses our new <code>UploadProgressBar</code> component, keeps track of the current file upload progress, changes the button styling and adds a new button to be explicit about starting an upload:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [upload, setUpload] = useState&lt;Uploader | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [progress, setProgress] = useState(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = [ ...e.target.files ][<span class="hljs-number">0</span>];

    <span class="hljs-keyword">const</span> uploader = <span class="hljs-keyword">new</span> Uploader({ file })
    .onProgress(<span class="hljs-function">(<span class="hljs-params">{ percentage }</span>) =&gt;</span> {
      setProgress(percentage);
    })
    .onComplete(<span class="hljs-function">(<span class="hljs-params">uploadResponse</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload complete'</span>, uploadResponse);
    })
    .onError(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'upload error'</span>, error)
    });

    setUpload(uploader);
  };

  <span class="hljs-keyword">const</span> uploadClicked = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!upload) { <span class="hljs-keyword">return</span> }

    upload.start();
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;div className=<span class="hljs-string">"flex text-sm text-gray-600"</span>&gt;
        &lt;label
          htmlFor=<span class="hljs-string">"file-upload"</span>
          className=<span class="hljs-string">"inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        &gt;
          &lt;div className=<span class="hljs-string">"text-md"</span>&gt;
            Choose File
          &lt;/div&gt;
          &lt;input
            id=<span class="hljs-string">"file-upload"</span>
            name=<span class="hljs-string">"files"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
            className=<span class="hljs-string">"sr-only"</span>
            onChange={onFileChanged}
            value={inputValue}
          /&gt;
        &lt;/label&gt;
      &lt;/div&gt;

      &lt;p className=<span class="hljs-string">"py-2 text-sm text-gray-500"</span>&gt;
        Any file up to <span class="hljs-number">5</span>TB
      &lt;/p&gt;

      {upload &amp;&amp; (
        &lt;div className=<span class="hljs-string">"py-2 flex flex-grow flex-col"</span>&gt;
          &lt;span className=<span class="hljs-string">"text-sm font-medium text-gray-900"</span>&gt;
            { upload.file.name }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { upload.file.type }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { prettyBytes(upload.file.size) }
          &lt;/span&gt;

          &lt;UploadProgressBar
            progress={progress}
          /&gt;
        &lt;/div&gt;
      )}

      &lt;button
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span>
        className=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        onClick={uploadClicked}
      &gt;
        Upload File
      &lt;/button&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>Before we can test this locally, we need to set up some ENV vars for our Next.js application like we did in Vercel. Let's create an <code>.env.local</code> file. This file is ignored in our <code>.gitignore</code> so we can put our secrets there. You will need to replace the <code>upload_bucket</code> variable with the bucket you created in the previous article.</p>
<pre><code class="lang-typescript">bucket_region=us-east<span class="hljs-number">-1</span>
upload_bucket=danoph-file-uploader-demo
access_key_id=$AWS_ACCESS_KEY_ID
access_key_secret=$AWS_SECRET_ACCESS_KEY
</code></pre>
<p>I noticed while writing this article that we could have run a command to pull the variables into our Vercel deployment, but then we would be using the same AWS access key ID and secret that we used to deploy our terraform. That would not be best practice security-wise so I'm kinda glad we didn't do it that way; however I think it is fine for local development.</p>
<p>Adding these values and restarting our Next.js application should allow us to upload a file locally directly to s3. After restarting the local server, choose a file and click the <code>Upload File</code> button. You should see upload progress now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677611696098/0ce08c5b-0686-4e8d-9d3e-b84f3fcc06bc.png" alt class="image--center mx-auto" /></p>
<p><strong>Side note</strong> - If you followed along with the previous article and you're seeing funky progress updates, I noticed there was a bug in the Uploader class from the previous article that had one small typo where <code>part.partNumber</code> should be <code>part.PartNumber</code>. Change line 228 of <code>lib/Uploader.ts</code> to look like this:</p>
<pre><code class="lang-typescript">        <span class="hljs-keyword">const</span> progressListener = <span class="hljs-built_in">this</span>.handleProgress.bind(<span class="hljs-built_in">this</span>, part.PartNumber - <span class="hljs-number">1</span>)
</code></pre>
<h2 id="heading-adding-drag-and-drop-support">Adding drag and drop support</h2>
<p>Adding drag and drop support is pretty simple. We don't need any external libraries but we will need to add some handlers in our component to handle the file drag, the file drop, showing when a file is being dragged over the drop area, and we will need to slightly refactor our <code>onFileChanged</code> to use a new function that we can also use for the drop file event. Here is what it will look like when we drag and drop a file onto the target:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677529277371/3aac9213-cfea-41fd-900c-1667b9762f68.gif" alt class="image--center mx-auto" /></p>
<p>Our <code>Home</code> component code looks like this now:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [upload, setUpload] = useState&lt;Uploader | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [progress, setProgress] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> [draggingOver, setDraggingOver] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> addFile = <span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> uploader = <span class="hljs-keyword">new</span> Uploader({ file })
    .onProgress(<span class="hljs-function">(<span class="hljs-params">{ percentage }</span>) =&gt;</span> {
      setProgress(percentage);
    })
    .onComplete(<span class="hljs-function">(<span class="hljs-params">uploadResponse</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload complete'</span>, uploadResponse);
    })
    .onError(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'upload error'</span>, error)
    });

    setUpload(uploader);
  };

  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = [ ...e.target.files ][<span class="hljs-number">0</span>];
    addFile(file);
  };

  <span class="hljs-keyword">const</span> uploadClicked = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!upload) { <span class="hljs-keyword">return</span> }

    upload.start();
  };

  <span class="hljs-keyword">const</span> stopEvent = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    e.preventDefault();
    e.stopPropagation();
  }

  <span class="hljs-keyword">const</span> handleDragEnter = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
  };

  <span class="hljs-keyword">const</span> handleDragLeave = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">const</span> handleDragOver = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">true</span>);
  };

  <span class="hljs-keyword">const</span> handleDrop = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> file = [ ...e.dataTransfer.files ][<span class="hljs-number">0</span>];
    addFile(file);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;div className=<span class="hljs-string">"flex text-sm text-gray-600"</span>&gt;
        &lt;div className=<span class="hljs-string">"w-full"</span>&gt;
          &lt;div
            className={<span class="hljs-string">`<span class="hljs-subst">${draggingOver
              ? <span class="hljs-string">"border-blue-500"</span>
              : <span class="hljs-string">"border-gray-300"</span>
            }</span> mt-1 flex items-center justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md`</span>}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            onDragEnter={handleDragEnter}
            onDragLeave={handleDragLeave}
          &gt;
            &lt;label
              htmlFor=<span class="hljs-string">"file-upload"</span>
              className=<span class="hljs-string">"inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
            &gt;
              &lt;div className=<span class="hljs-string">"text-md"</span>&gt;
                Choose File
              &lt;/div&gt;
              &lt;input
                id=<span class="hljs-string">"file-upload"</span>
                name=<span class="hljs-string">"files"</span>
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
                className=<span class="hljs-string">"sr-only"</span>
                onChange={onFileChanged}
                value={inputValue}
              /&gt;
            &lt;/label&gt;
            &lt;p className=<span class="hljs-string">"pl-1 text-sm"</span>&gt;or drag and drop&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;p className=<span class="hljs-string">"py-2 text-sm text-gray-500"</span>&gt;
        Any file up to <span class="hljs-number">5</span>TB
      &lt;/p&gt;

      {upload &amp;&amp; (
        &lt;div className=<span class="hljs-string">"py-2 flex flex-grow flex-col"</span>&gt;
          &lt;span className=<span class="hljs-string">"text-sm font-medium text-gray-900"</span>&gt;
            { upload.file.name }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { upload.file.type }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { prettyBytes(upload.file.size) }
          &lt;/span&gt;

          &lt;UploadProgressBar
            progress={progress}
          /&gt;
        &lt;/div&gt;
      )}

      &lt;button
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span>
        className=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        onClick={uploadClicked}
      &gt;
        Upload File
      &lt;/button&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>Some things to note:</p>
<ul>
<li><p>The <code>draggingOver</code> state keeps track of when the file is being dragged over the drop area.</p>
</li>
<li><p>The <code>onFileChanged</code> calls a new function <code>addFile</code> which just takes a file as an input and sets the current upload to that file.</p>
</li>
<li><p>The <code>handleDrop</code> function uses the newly added <code>addFile</code> function and passes in the event's file.</p>
</li>
<li><p>The <code>handleDragEnter</code>, <code>handleDragLeave</code>, <code>handleDragOver</code> handle the styling of the drop area.</p>
</li>
<li><p>I added a new wrapper div around the file upload form part that triggers the drag events.</p>
</li>
</ul>
<p>And that's it. Super simple to add drag and drop support to our existing file upload form.</p>
<h2 id="heading-converting-our-single-file-uploader-to-a-multi-file-uploader">Converting our single file uploader to a multi file uploader</h2>
<p>It would be nice to allow people to drag and drop or select multiple files when uploading to our application. So we're going to make a few changes to allow for that. Let's change our <code>upload</code> state to <code>uploads</code>, add <code>multiple</code> to the file input in our view, and change our functions and variables to reflect the multiple uploads. We have another issue, where our <code>progress</code> state is tied to one upload. There are many different ways to do this, like having a separate array/hash for each upload's progress, modifying the Uploader class to keep track of its own progress (sometimes you can't do this if you're using external libraries), or making sure each upload in our array has progress alongside it by encapsulating the uploads in the array in a wrapper object, like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> FileUpload {
  uploader: Uploader;
  progress: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [uploads, setUploads] = useState&lt;FileUpload[]&gt;([]);
</code></pre>
<p>Let's choose the <code>FileUpload</code> path and modify all of our functions to handle multiple file uploads. So our <code>Home</code> component looks like this now:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> FileUpload {
  uploader: Uploader;
  progress: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [uploads, setUploads] = useState&lt;FileUpload[]&gt;([]);
  <span class="hljs-keyword">const</span> [draggingOver, setDraggingOver] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> updateProgress = <span class="hljs-function">(<span class="hljs-params">filename, percentage</span>) =&gt;</span> {
    setUploads(<span class="hljs-function"><span class="hljs-params">state</span> =&gt;</span>
      state.map(<span class="hljs-function"><span class="hljs-params">fileUpload</span> =&gt;</span>
        fileUpload.uploader.file.name === filename
        ? { ...fileUpload, progress: percentage }
        : fileUpload
      )
    );
  };

  <span class="hljs-keyword">const</span> addFiles = <span class="hljs-function"><span class="hljs-params">files</span> =&gt;</span> {
    setUploads(
      files.map(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> ({
        uploader: <span class="hljs-keyword">new</span> Uploader({ file })
        .onProgress(<span class="hljs-function">(<span class="hljs-params">{ percentage }</span>) =&gt;</span> {
          updateProgress(file.name, percentage);
        })
        .onComplete(<span class="hljs-function">(<span class="hljs-params">uploadResponse</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload complete'</span>, uploadResponse);
        })
        .onError(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'upload error'</span>, error)
        }),
        progress: <span class="hljs-number">0</span>
      }))
    );
  };

  <span class="hljs-keyword">const</span> onFilesChanged = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> files = [ ...e.target.files ];
    addFiles(files);
  };

  <span class="hljs-keyword">const</span> uploadClicked = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!uploads.length) { <span class="hljs-keyword">return</span> }

    uploads.forEach(<span class="hljs-function"><span class="hljs-params">upload</span> =&gt;</span> upload.uploader.start());
  };

  <span class="hljs-keyword">const</span> stopEvent = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    e.preventDefault();
    e.stopPropagation();
  }

  <span class="hljs-keyword">const</span> handleDragEnter = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
  };

  <span class="hljs-keyword">const</span> handleDragLeave = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">const</span> handleDragOver = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">true</span>);
  };

  <span class="hljs-keyword">const</span> handleDrop = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    stopEvent(e);
    setDraggingOver(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> files = [ ...e.dataTransfer.files ];
    addFiles(files);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;div className=<span class="hljs-string">"flex text-sm text-gray-600"</span>&gt;
        &lt;div className=<span class="hljs-string">"w-full"</span>&gt;
          &lt;div
            className={<span class="hljs-string">`<span class="hljs-subst">${draggingOver
              ? <span class="hljs-string">"border-blue-500"</span>
              : <span class="hljs-string">"border-gray-300"</span>
            }</span> mt-1 flex items-center justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md`</span>}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            onDragEnter={handleDragEnter}
            onDragLeave={handleDragLeave}
          &gt;
            &lt;label
              htmlFor=<span class="hljs-string">"file-upload"</span>
              className=<span class="hljs-string">"inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
            &gt;
              &lt;div className=<span class="hljs-string">"text-md"</span>&gt;
                Choose File
              &lt;/div&gt;
              &lt;input
                id=<span class="hljs-string">"file-upload"</span>
                name=<span class="hljs-string">"files"</span>
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
                className=<span class="hljs-string">"sr-only"</span>
                onChange={onFilesChanged}
                multiple
                value={inputValue}
              /&gt;
            &lt;/label&gt;
            &lt;p className=<span class="hljs-string">"pl-1 text-sm"</span>&gt;or drag and drop&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;p className=<span class="hljs-string">"py-2 text-sm text-gray-500"</span>&gt;
        Any file up to <span class="hljs-number">5</span>TB
      &lt;/p&gt;

      {uploads.map(<span class="hljs-function">(<span class="hljs-params">{ uploader: { file }, progress }</span>) =&gt;</span> (
        &lt;div key={file.name} className=<span class="hljs-string">"py-2 flex flex-grow flex-col"</span>&gt;
          &lt;span className=<span class="hljs-string">"text-sm font-medium text-gray-900"</span>&gt;
            { file.name }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { file.type }
          &lt;/span&gt;
          &lt;span className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;
            { prettyBytes(file.size) }
          &lt;/span&gt;

          &lt;UploadProgressBar
            progress={progress}
          /&gt;
        &lt;/div&gt;
      ))}

      &lt;button
        <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span>
        className=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        onClick={uploadClicked}
      &gt;
        Upload File
      &lt;/button&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>We had to make a few changes but nothing very complicated. Some things to note:</p>
<ul>
<li><p>The state variable <code>uploads</code> changed to an array of <code>FileUpload</code> objects which we defined in our new TypeScript interface.</p>
</li>
<li><p>We had to change the <code>handleDrop</code> and renamed <code>onFileChanged</code> to <code>onFilesChanged</code> to reflect having multiple uploads.</p>
</li>
<li><p>We added <code>multiple</code> to the file input to allow multiple files.</p>
</li>
<li><p>The <code>addFiles</code> function iterates through the array of dropped or selected files and sets the uploads to an array of <code>FileUpload</code> objects.</p>
</li>
<li><p>We added the <code>updateProgress</code> function that the Uploader <code>onProgress</code> callback uses to find an upload in the array by the file's name, then updating the progress for that specific one.</p>
</li>
<li><p>The <code>uploadClicked</code> changed to iterate through each upload and begin the upload.</p>
</li>
<li><p>The view changed slightly to iterate over each upload in the <code>uploads</code> array and display the same output like when we had a single upload.</p>
</li>
</ul>
<p>You should be able to drag and drop or select multiple files and see progress for each individual file now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677611324585/151d50be-a866-41be-8d26-0c866d7f1a87.png" alt class="image--center mx-auto" /></p>
<p>And that's it for now!</p>
<p>The full source code for this example is here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload">https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Upload Large Files Directly to Amazon S3 in React/Next.js]]></title><description><![CDATA[If you've ever needed to upload large files over 5 gigabytes to Amazon S3, you will run into a road block pretty quickly. Using multipart chunked file uploads is the way to get around that. Also, if you've built a file uploader before, you have to ma...]]></description><link>https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs</link><guid isPermaLink="true">https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs</guid><category><![CDATA[Vercel]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Amazon S3]]></category><category><![CDATA[File Upload]]></category><dc:creator><![CDATA[Daniel Errante]]></dc:creator><pubDate>Fri, 17 Feb 2023 20:28:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/f77Bh3inUpE/upload/f2fcb5954a702060c58c345f6a955298.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've ever needed to upload large files over 5 gigabytes to Amazon S3, you will run into a road block pretty quickly. Using multipart chunked file uploads is the way to get around that. Also, if you've built a file uploader before, you have to make the decision of whether the file upload should proxy through your API backend or go directly to S3. Proxying through your API backend is simpler to set up but has some drawbacks, such as tying up a server process during the file upload, and having to handle the file on the server's file system before sending it over to a file storage system like S3. It is also usually a lot slower than uploading directly to S3.</p>
<p>Uploading directly to s3 still requires some type of backend that can create presigned S3 URLs for the web front end since you don't want the front end being able to generate those on its own. Next.js provides a convenient way of doing that with API endpoints you can create that get deployed as edge functions.</p>
<p>In this tutorial, we are going to create a simple file uploader from scratch in Next.js using Tailwind CSS and TypeScript. We will also be using terraform to set up the necessary AWS resources to store the files we are uploading. We will also be deploying this app to Vercel. It sounds like a lot of moving parts but you can have everything working by following the steps in this tutorial in less than an hour.</p>
<p>The full source code for this application is here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo">https://github.com/danoph/file-uploader-demo</a></p>
<h3 id="heading-create-a-new-nextjs-app-with-typescript">Create a new Next.js app with TypeScript:</h3>
<pre><code class="lang-bash">npx create-next-app@latest file-uploader-demo --typescript --eslint
<span class="hljs-built_in">cd</span> file-uploader-demo
</code></pre>
<h3 id="heading-install-tailwind-css">Install Tailwind CSS:</h3>
<p>Full instructions are here: <a target="_blank" href="https://tailwindcss.com/docs/guides/nextjs">https://tailwindcss.com/docs/guides/nextjs</a></p>
<pre><code class="lang-bash">npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
</code></pre>
<p>I chose not to use the "src" directory or the experimental "app" directory in the prompts when setting up NextJS. So, we will need to change the <code>tailwind.config.js</code> file slightly:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">content</span>: [
    <span class="hljs-string">"./pages/**/*.{js,ts,jsx,tsx}"</span>,
    <span class="hljs-string">"./components/**/*.{js,ts,jsx,tsx}"</span>,
  ],
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {},
  },
  <span class="hljs-attr">plugins</span>: [],
}
</code></pre>
<p>In the <code>styles/globals.css</code> file, we want to remove the default NextJS styling and replace it with some tailwind modules, as well as a small styling hack that most of the TailwindUI templates need:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;

<span class="hljs-selector-id">#__next</span> {
  @apply flex flex-col h-full;
  // some tailwind templates need this line <span class="hljs-attribute">instead</span>:
  // min-height: <span class="hljs-number">100%</span>;
}
</code></pre>
<p>Run the app locally:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p>Edit the main <code>pages/index.tsx</code> file and replace it with a simple Tailwind CSS component to make sure everything's working:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;h1 className=<span class="hljs-string">"text-3xl font-bold underline"</span>&gt;
      Hello world!
    &lt;/h1&gt;
  )
}
</code></pre>
<p>When you visit the page in your browser the text should be large and underlined. I'm also going to add <code>"noImplicitAny": false</code> to the <code>tsconfig.json</code> file to get rid of annoying "No implicit any" TypeScript warnings and <code>"downlevelIteration": true</code> for a convenience function I'm using in a library we will see later in the article. So your <code>tsconfig.json</code> file should look like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"es5"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"dom"</span>, <span class="hljs-string">"dom.iterable"</span>, <span class="hljs-string">"esnext"</span>],
    <span class="hljs-attr">"allowJs"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"downlevelIteration"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noImplicitAny"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"resolveJsonModule"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"jsx"</span>: <span class="hljs-string">"preserve"</span>,
    <span class="hljs-attr">"incremental"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"."</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"@/*"</span>: [<span class="hljs-string">"./*"</span>]
    }
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"next-env.d.ts"</span>, <span class="hljs-string">"**/*.ts"</span>, <span class="hljs-string">"**/*.tsx"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>]
}
</code></pre>
<h3 id="heading-time-to-create-the-uploader">Time to create the uploader</h3>
<p>There are a few moving pieces to this part. We will need an upload form (the easy part) and 3 separate endpoints in our Next.js app to handle:</p>
<ol>
<li><p><code>POST /api/multipart_uploads</code><br /> Creating a "multipart upload" for S3. This gives you a unique file ID for the upload.</p>
</li>
<li><p><code>POST /api/multipart_uploads/{fileId}/part_url</code><br /> Since we might be uploading large files, we want to split up the upload into separate chunks, so for each individual chunk, we need to generate a pre-signed URL for that chunk.</p>
</li>
<li><p><code>POST /api/multipart_uploads/{fileId}/completions</code><br /> When all of the parts have been uploaded, we "complete" the multipart upload and AWS stitches together all of the chunks that we uploaded into a single file.</p>
</li>
</ol>
<p>I am also going to use an Uploader TypeScript class that I found here: (<a target="_blank" href="https://github.com/pilovm/multithreaded-uploader/blob/master/frontend/uploader.js">https://github.com/pilovm/multithreaded-uploader/blob/master/frontend/uploader.js</a>). I had to modify the Uploader to call our Next.js API endpoints that we will be making and also to overcome an obstacle with the original uploader that does not account for pre-signed URLs expiring if an upload takes longer than the expiration timeout for each multipart upload chunk URL. The class also uses <code>axios</code> so we need to add that library to our project:</p>
<pre><code class="lang-bash">npm i axios
</code></pre>
<p>Here is my uploader class that I modified to work with our Next.js app. Let's make a folder called <code>lib</code> and we will put this Uploader class in <code>lib/Uploader.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/Uploader.ts</span>

<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>

<span class="hljs-keyword">const</span> API_BASE_URL = <span class="hljs-string">"/api/"</span>;

<span class="hljs-keyword">const</span> api = axios.create({
  baseURL: API_BASE_URL,
});

<span class="hljs-keyword">interface</span> Part {
  ETag: <span class="hljs-built_in">string</span>
  PartNumber: <span class="hljs-built_in">number</span>
}

<span class="hljs-keyword">interface</span> IOptions {
  chunkSize?: <span class="hljs-built_in">number</span>;
  threadsQuantity?: <span class="hljs-built_in">number</span>;
  file: File;
}

<span class="hljs-comment">// original source: https://github.com/pilovm/multithreaded-uploader/blob/master/frontend/uploader.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Uploader {
  chunkSize: <span class="hljs-built_in">number</span>;
  threadsQuantity: <span class="hljs-built_in">number</span>;
  file: File;
  aborted: <span class="hljs-built_in">boolean</span>;
  uploadedSize: <span class="hljs-built_in">number</span>;
  progressCache: <span class="hljs-built_in">any</span>;
  activeConnections: <span class="hljs-built_in">any</span>;
  parts: <span class="hljs-built_in">any</span>[];
  uploadedParts: <span class="hljs-built_in">any</span>[];
  uploadId: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  fileKey: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  onProgressFn: <span class="hljs-function">(<span class="hljs-params">progress</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  onErrorFn: <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  onCompleteFn: <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> <span class="hljs-built_in">void</span>;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">options: IOptions</span>) {
    <span class="hljs-comment">// this must be bigger than or equal to 5MB,</span>
    <span class="hljs-comment">// otherwise AWS will respond with:</span>
    <span class="hljs-comment">// "Your proposed upload is smaller than the minimum allowed size"</span>
    <span class="hljs-built_in">this</span>.chunkSize = options.chunkSize || <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span> * <span class="hljs-number">5</span>
    <span class="hljs-comment">// number of parallel uploads</span>
    <span class="hljs-built_in">this</span>.threadsQuantity = <span class="hljs-built_in">Math</span>.min(options.threadsQuantity || <span class="hljs-number">5</span>, <span class="hljs-number">15</span>)
    <span class="hljs-built_in">this</span>.file = options.file
    <span class="hljs-built_in">this</span>.aborted = <span class="hljs-literal">false</span>
    <span class="hljs-built_in">this</span>.uploadedSize = <span class="hljs-number">0</span>
    <span class="hljs-built_in">this</span>.progressCache = {}
    <span class="hljs-built_in">this</span>.activeConnections = {}
    <span class="hljs-built_in">this</span>.parts = []
    <span class="hljs-built_in">this</span>.uploadedParts = []
    <span class="hljs-built_in">this</span>.uploadId = <span class="hljs-literal">null</span>
    <span class="hljs-built_in">this</span>.fileKey = <span class="hljs-literal">null</span>
    <span class="hljs-built_in">this</span>.onProgressFn = <span class="hljs-function">(<span class="hljs-params">progress</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'progress'</span>, progress);
    <span class="hljs-built_in">this</span>.onErrorFn = <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, err);
    <span class="hljs-built_in">this</span>.onCompleteFn = <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'response'</span>, response);
  }

  start() {
    <span class="hljs-built_in">this</span>.initialize()
  }

  <span class="hljs-keyword">async</span> initialize() {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data: { uploadId, fileKey } } = <span class="hljs-keyword">await</span> api.request({
        url: <span class="hljs-string">"/multipart_uploads"</span>,
        method: <span class="hljs-string">"POST"</span>,
        data: {
          filename: <span class="hljs-built_in">this</span>.file.name,
        },
      })

      <span class="hljs-built_in">this</span>.uploadId = uploadId;
      <span class="hljs-built_in">this</span>.fileKey = fileKey;

      <span class="hljs-keyword">const</span> numberOfParts = <span class="hljs-built_in">Math</span>.ceil(<span class="hljs-built_in">this</span>.file.size / <span class="hljs-built_in">this</span>.chunkSize)

      <span class="hljs-built_in">this</span>.parts.push(
        ...[...Array(numberOfParts).keys()].map(<span class="hljs-function">(<span class="hljs-params">val, index</span>) =&gt;</span> ({
          PartNumber: index + <span class="hljs-number">1</span>
        }))
      );

      <span class="hljs-built_in">this</span>.sendNext();
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.complete(error)
    }
  }

  sendNext() {
    <span class="hljs-keyword">const</span> activeConnections = <span class="hljs-built_in">Object</span>.keys(<span class="hljs-built_in">this</span>.activeConnections).length

    <span class="hljs-keyword">if</span> (activeConnections &gt;= <span class="hljs-built_in">this</span>.threadsQuantity) {
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.parts.length) {
      <span class="hljs-keyword">if</span> (!activeConnections) {
        <span class="hljs-built_in">this</span>.complete()
      }

      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">const</span> part = <span class="hljs-built_in">this</span>.parts.pop();

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.file &amp;&amp; part) {
      <span class="hljs-keyword">const</span> sentSize = (part.PartNumber - <span class="hljs-number">1</span>) * <span class="hljs-built_in">this</span>.chunkSize
      <span class="hljs-keyword">const</span> chunk = <span class="hljs-built_in">this</span>.file.slice(sentSize, sentSize + <span class="hljs-built_in">this</span>.chunkSize)

      <span class="hljs-keyword">const</span> sendChunkStarted = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">this</span>.sendNext()
      }

      <span class="hljs-built_in">this</span>.sendChunk(chunk, part, sendChunkStarted)
        .then(<span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">this</span>.sendNext()
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          <span class="hljs-built_in">this</span>.parts.push(part)
          <span class="hljs-built_in">this</span>.complete(error)
        })
    }
  }

  <span class="hljs-comment">// terminating the multipart upload request on success or failure</span>
  <span class="hljs-keyword">async</span> complete(error: unknown | <span class="hljs-literal">undefined</span> = <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">if</span> (error &amp;&amp; !<span class="hljs-built_in">this</span>.aborted) {
      <span class="hljs-built_in">this</span>.onErrorFn(error)
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">this</span>.onErrorFn(error)
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.sendCompleteRequest()
      <span class="hljs-built_in">this</span>.onCompleteFn(response);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">this</span>.onErrorFn(error)
    }
  }

  <span class="hljs-comment">// finalizing the multipart upload request on success by calling</span>
  <span class="hljs-comment">// the finalization API</span>
  <span class="hljs-keyword">async</span> sendCompleteRequest() {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.uploadId &amp;&amp; <span class="hljs-built_in">this</span>.fileKey) {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> api.request({
        url: <span class="hljs-string">`/multipart_uploads/<span class="hljs-subst">${<span class="hljs-built_in">this</span>.uploadId}</span>/completions`</span>,
        method: <span class="hljs-string">"POST"</span>,
        data: {
          fileKey: <span class="hljs-built_in">this</span>.fileKey,
          parts: <span class="hljs-built_in">this</span>.uploadedParts,
        },
      })

      <span class="hljs-keyword">return</span> response.data;
    }
  }

  sendChunk(chunk, part, sendChunkStarted): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      <span class="hljs-built_in">this</span>.upload(chunk, part, sendChunkStarted)
        .then(<span class="hljs-function">(<span class="hljs-params">status</span>) =&gt;</span> {
          <span class="hljs-keyword">if</span> (status !== <span class="hljs-number">200</span>) {
            reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Failed chunk upload"</span>))
            <span class="hljs-keyword">return</span>
          }

          resolve()
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          reject(error)
        })
    })
  }

  <span class="hljs-comment">// calculating the current progress of the multipart upload request</span>
  handleProgress(part, event) {
    <span class="hljs-comment">//console.log('part', part, 'event', event);</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.file) {
      <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"progress"</span> || event.type === <span class="hljs-string">"error"</span> || event.type === <span class="hljs-string">"abort"</span>) {
        <span class="hljs-built_in">this</span>.progressCache[part] = event.loaded
      }

      <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"uploaded"</span>) {
        <span class="hljs-built_in">this</span>.uploadedSize += <span class="hljs-built_in">this</span>.progressCache[part] || <span class="hljs-number">0</span>
        <span class="hljs-keyword">delete</span> <span class="hljs-built_in">this</span>.progressCache[part]
      }

      <span class="hljs-keyword">const</span> inProgress = <span class="hljs-built_in">Object</span>.keys(<span class="hljs-built_in">this</span>.progressCache)
        .map(<span class="hljs-built_in">Number</span>)
        .reduce(<span class="hljs-function">(<span class="hljs-params">memo, id</span>) =&gt;</span> (memo += <span class="hljs-built_in">this</span>.progressCache[id]), <span class="hljs-number">0</span>)

      <span class="hljs-keyword">const</span> sent = <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">this</span>.uploadedSize + inProgress, <span class="hljs-built_in">this</span>.file.size)

      <span class="hljs-keyword">const</span> total = <span class="hljs-built_in">this</span>.file.size

      <span class="hljs-keyword">const</span> percentage = <span class="hljs-built_in">Math</span>.round((sent / total) * <span class="hljs-number">100</span>)

      <span class="hljs-built_in">this</span>.onProgressFn({
        sent: sent,
        total: total,
        percentage: percentage,
      })
    }
  }

  upload(file, part, sendChunkStarted) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-keyword">async</span> (resolve, reject) =&gt; {
      <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.uploadId &amp;&amp; <span class="hljs-built_in">this</span>.fileKey) {
        <span class="hljs-comment">// we need to get the multipart chunk url immediately before starting the upload</span>
        <span class="hljs-comment">// since creating them beforehand may result in the urls expiring</span>
        <span class="hljs-keyword">const</span> { data: { signedUrl } } = <span class="hljs-keyword">await</span> api.request({
          url: <span class="hljs-string">`/multipart_uploads/<span class="hljs-subst">${<span class="hljs-built_in">this</span>.uploadId}</span>/part_url`</span>,
          method: <span class="hljs-string">"POST"</span>,
          data: {
            fileKey: <span class="hljs-built_in">this</span>.fileKey,
            partNumber: part.PartNumber,
          }
        })

        <span class="hljs-comment">// - 1 because PartNumber is an index starting from 1 and not 0</span>
        <span class="hljs-keyword">const</span> xhr = (<span class="hljs-built_in">this</span>.activeConnections[part.PartNumber - <span class="hljs-number">1</span>] = <span class="hljs-keyword">new</span> XMLHttpRequest())

        sendChunkStarted()

        <span class="hljs-keyword">const</span> progressListener = <span class="hljs-built_in">this</span>.handleProgress.bind(<span class="hljs-built_in">this</span>, part.PartNumber - <span class="hljs-number">1</span>)

        xhr.upload.addEventListener(<span class="hljs-string">"progress"</span>, progressListener)

        xhr.addEventListener(<span class="hljs-string">"error"</span>, progressListener)
        xhr.addEventListener(<span class="hljs-string">"abort"</span>, progressListener)
        xhr.addEventListener(<span class="hljs-string">"loadend"</span>, progressListener)

        xhr.open(<span class="hljs-string">"PUT"</span>, signedUrl)

        xhr.onreadystatechange = <span class="hljs-function">() =&gt;</span> {
          <span class="hljs-keyword">if</span> (xhr.readyState === <span class="hljs-number">4</span> &amp;&amp; xhr.status === <span class="hljs-number">200</span>) {
            <span class="hljs-comment">// retrieving the ETag parameter from the HTTP headers</span>
            <span class="hljs-keyword">const</span> ETag = xhr.getResponseHeader(<span class="hljs-string">"etag"</span>)

            <span class="hljs-keyword">if</span> (ETag) {
              <span class="hljs-keyword">const</span> uploadedPart = {
                PartNumber: part.PartNumber,
                <span class="hljs-comment">// removing the " enclosing carachters from</span>
                <span class="hljs-comment">// the raw ETag</span>
                ETag: ETag.replaceAll(<span class="hljs-string">'"'</span>, <span class="hljs-string">""</span>),
              }

              <span class="hljs-built_in">this</span>.uploadedParts.push(uploadedPart)

              resolve(xhr.status)
              <span class="hljs-keyword">delete</span> <span class="hljs-built_in">this</span>.activeConnections[part.PartNumber - <span class="hljs-number">1</span>]
            }
          }
        }

        xhr.onerror = <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'xhr error'</span>, error);
          reject(error)
          <span class="hljs-keyword">delete</span> <span class="hljs-built_in">this</span>.activeConnections[part.PartNumber - <span class="hljs-number">1</span>]
        }

        xhr.onabort = <span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'xhr abort'</span>);
          reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Upload canceled by user"</span>))
          <span class="hljs-keyword">delete</span> <span class="hljs-built_in">this</span>.activeConnections[part.PartNumber - <span class="hljs-number">1</span>]
        }

        xhr.send(file)
      }
    })
  }

  onProgress(onProgress) {
    <span class="hljs-built_in">this</span>.onProgressFn = onProgress
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>
  }

  onComplete(onComplete) {
    <span class="hljs-built_in">this</span>.onCompleteFn = onComplete
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>
  }

  onError(onError) {
    <span class="hljs-built_in">this</span>.onErrorFn = onError
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>
  }

  abort() {
    <span class="hljs-built_in">Object</span>.keys(<span class="hljs-built_in">this</span>.activeConnections)
      .map(<span class="hljs-built_in">Number</span>)
      .forEach(<span class="hljs-function">(<span class="hljs-params">id</span>) =&gt;</span> {
        <span class="hljs-built_in">this</span>.activeConnections[id].abort()
      })

    <span class="hljs-built_in">this</span>.aborted = <span class="hljs-literal">true</span>
  }
}
</code></pre>
<p>After adding this file, let's create an upload form and wire up the front end to use this new Uploader class.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/index.tsx</span>

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Uploader } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/Uploader'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [upload, setUpload] = useState&lt;Uploader | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> onFileChanged = <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> file = [ ...e.target.files ][<span class="hljs-number">0</span>];
    <span class="hljs-keyword">const</span> uploader = <span class="hljs-keyword">new</span> Uploader({ file })
    .onProgress(<span class="hljs-function">(<span class="hljs-params">{ percentage }</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload progress'</span>, percentage);
    })
    .onComplete(<span class="hljs-function">(<span class="hljs-params">uploadResponse</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'upload complete'</span>, uploadResponse);
    })
    .onError(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'upload error'</span>, error)
    });

    setUpload(uploader);

    uploader.start();
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"mx-auto max-w-7xl sm:p-6 lg:p-8"</span>&gt;
      &lt;div className=<span class="hljs-string">"flex text-sm text-gray-600"</span>&gt;
        &lt;label
          htmlFor=<span class="hljs-string">"file-upload"</span>
          className=<span class="hljs-string">"inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
        &gt;
          &lt;div className=<span class="hljs-string">"text-lg"</span>&gt;
            Upload a file
          &lt;/div&gt;
          &lt;input
            id=<span class="hljs-string">"file-upload"</span>
            name=<span class="hljs-string">"files"</span>
            <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
            className=<span class="hljs-string">"sr-only"</span>
            onChange={onFileChanged}
            value={inputValue}
          /&gt;
        &lt;/label&gt;
      &lt;/div&gt;

      &lt;p className=<span class="hljs-string">"py-2 text-sm text-gray-500"</span>&gt;
        Any file up to <span class="hljs-number">5</span>TB
      &lt;/p&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>Your page should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676657419332/2839d2f5-65e0-4261-a19b-3e8c62903c21.png" alt class="image--center mx-auto" /></p>
<p>Now we need to add those 3 endpoints for creating a multipart upload, creating a multipart part upload url, and completing the multipart upload. We will need to add two npm packages to our project: <code>@aws-sdk/client-s3</code> and <code>@aws-sdk/s3-request-presigner</code>:</p>
<pre><code class="lang-bash">npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
</code></pre>
<p>To simplify creating our 3 endpoints, I've created a few TypeScript functions that will assist us. We will put this file in <code>lib/s3.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/s3.ts</span>

<span class="hljs-keyword">import</span> {
    S3Client,
    CreateMultipartUploadCommand,
    UploadPartCommand,
    CompleteMultipartUploadCommand,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-s3"</span>;
<span class="hljs-keyword">import</span> { getSignedUrl } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/s3-request-presigner"</span>;

<span class="hljs-keyword">const</span> REGION = process.env.bucket_region;
<span class="hljs-keyword">const</span> UPLOAD_BUCKET = process.env.upload_bucket;

<span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> these are named differently than the normal AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY</span>
<span class="hljs-comment">// because Vercel does not allow you to set those environment variables for a deployment</span>
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> S3Client({
  region: REGION,
  credentials: {
    accessKeyId: <span class="hljs-string">`<span class="hljs-subst">${process.env.access_key_id}</span>`</span>,
    secretAccessKey: <span class="hljs-string">`<span class="hljs-subst">${process.env.access_key_secret}</span>`</span>,
  }
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createMultipartUpload = <span class="hljs-keyword">async</span> ({ filename }) =&gt; {
  <span class="hljs-keyword">const</span> { Key, UploadId } = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> CreateMultipartUploadCommand({
      Bucket: UPLOAD_BUCKET,
      Key: filename,
      ACL: <span class="hljs-string">"private"</span>,
    })
  );

  <span class="hljs-keyword">return</span> {
    uploadId: UploadId,
    fileKey: Key,
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createMultipartUploadPart = <span class="hljs-keyword">async</span> ({ fileKey, uploadId, partNumber }) =&gt; {
  <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> UploadPartCommand({
    Bucket: UPLOAD_BUCKET,
    Key: fileKey,
    UploadId: uploadId,
    PartNumber: partNumber,
  });

  <span class="hljs-keyword">const</span> signedUrl = <span class="hljs-keyword">await</span> getSignedUrl(
    client <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>, <span class="hljs-comment">// avoiding typescript lint errors</span>
    command <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>, <span class="hljs-comment">// avoiding typescript lint errors</span>
    {
      expiresIn: <span class="hljs-number">3600</span>,
    }
  );

  <span class="hljs-keyword">return</span> {
    signedUrl
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> finishMultipartUpload = <span class="hljs-keyword">async</span> ({ fileKey, uploadId, parts }) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> CompleteMultipartUploadCommand({
      Bucket: UPLOAD_BUCKET,
      Key: fileKey,
      UploadId: uploadId,
      MultipartUpload: {
        Parts: parts.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> {
          <span class="hljs-keyword">if</span> (a.PartNumber &lt; b.PartNumber) {
            <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
          }

          <span class="hljs-keyword">if</span> (a.PartNumber &gt; b.PartNumber) {
            <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
          }

          <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
        })
      }
    })
  );

  <span class="hljs-keyword">return</span> response;
};
</code></pre>
<p>Now we can create our endpoints that will end up using these functions. Let's start with creating the multipart upload. We can try and keep this as RESTful as possible by creating the folder <code>pages/api/multipart_uploads</code> and then creating an <code>index.ts</code> file within that folder. This will handle creating the multipart upload:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/multipart_uploads/index.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { createMultipartUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-keyword">const</span> { filename } = req.body;
  <span class="hljs-keyword">const</span> { uploadId, fileKey } = <span class="hljs-keyword">await</span> createMultipartUpload({ filename });

  res.status(<span class="hljs-number">201</span>).json({
    uploadId,
    fileKey,
  });
}
</code></pre>
<p>Next, let's create the endpoint for creating a multipart upload chunk url. Let's make a folder called <code>[uploadId]</code> inside the <code>multipart_uploads</code> folder and name the file <code>part_url.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/multipart_uploads/[uploadId]/part_url.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { createMultipartUploadPart } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-keyword">const</span> { uploadId } = req.query;
  <span class="hljs-keyword">const</span> { fileKey, partNumber } = req.body;

  <span class="hljs-keyword">const</span> { signedUrl } = <span class="hljs-keyword">await</span> createMultipartUploadPart({
    fileKey,
    uploadId,
    partNumber
  });

  res.status(<span class="hljs-number">201</span>).json({
    signedUrl
  });
}
</code></pre>
<p>And finally let's create the endpoint for completing a multipart upload in <code>pages/api/multipart_uploads/[uploadId]/completions.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/api/multipart_uploads/[uploadId]/completions.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextApiRequest, NextApiResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> { finishMultipartUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/s3'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler</span>(<span class="hljs-params">req, res</span>) </span>{
  <span class="hljs-keyword">const</span> { uploadId } = req.query;
  <span class="hljs-keyword">const</span> { fileKey, parts } = req.body;

  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> finishMultipartUpload({
    fileKey,
    uploadId,
    parts
  });

  res.status(<span class="hljs-number">200</span>).json({});
}
</code></pre>
<p>These endpoints will not work out of the box because we need to set up our S3 bucket. Let's start writing some Terraform!</p>
<p>In order to create terraform for our new app, we need to manually create an s3 bucket that will house our terraform config. So, go into S3 in the AWS Console (https://aws.amazon.com) and create a new bucket and make sure <strong>Object Versioning is enabled</strong>. Since S3 bucket names have to be unique across all of AWS, I usually put my username or the app domain in the front of the bucket name to ensure it's unique. Don't use dots in the name of the bucket because that has other consequences that we don't want to run into for this tutorial. So, for example, for this application I am going to name my bucket <code>danoph-file-uploader-demo-terraform</code> and pick <code>us-east-1</code> for the region.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676661488220/48d876a2-5d6f-452b-8dd6-fb5cacf6c727.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676661518425/cfd2431c-51e9-40d5-8ed1-8545f93d1d69.png" alt class="image--center mx-auto" /></p>
<p>In order to run terraform locally, make sure you have Terraform installed. On MacOS you can run <code>brew install terraform</code> or use <code>tfenv</code> if you want to manage multiple versions. Make sure the version you are using is near <code>1.3.8</code>, which is the latest version as of this writing. You can check your version by running <code>terraform version</code></p>
<p>Also, you need to make sure you have programmatic access to AWS. If you are logged in as the root user in your AWS account, you can click on your name in the top right, and then click <code>Security Credentials</code>. Under the <code>Access Keys</code> section, click on <code>Create access key</code>. Make sure to download these credentials since you will only see them once. If you are more familiar with AWS, you probably know giving the root AWS user isn't the most secure thing to do. The more secure way to do this would be to create a new IAM user that we will use to run terraform locally, and restricting access to only the certain resources we need to manage in AWS. That is outside the scope of this article so I'm going to assume you have created your access key and secret and now you need to put them in your <code>~/.bash_profile</code> or <code>~/.zshrc</code> file.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> AWS_ACCESS_KEY_ID=<span class="hljs-string">"{value here}"</span>
<span class="hljs-built_in">export</span> AWS_SECRET_ACCESS_KEY=<span class="hljs-string">"{value here}"</span>
</code></pre>
<p>Make sure after adding these values to resource your <code>~/.zshrc</code> or <code>~/.bash_profile</code> by running <code>. ~/.zshrc</code> or <code>. ~/.bash_profile</code>.</p>
<p>Now that you have credentials set up, let's create some boilerplate terraform inside a <code>main.tf</code> file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># main.tf</span>

terraform {
  required_providers {
    aws = {
      <span class="hljs-built_in">source</span> = <span class="hljs-string">"hashicorp/aws"</span>
      version = <span class="hljs-string">"4.39.0"</span>
    }
  }
}

terraform {
  backend <span class="hljs-string">"s3"</span> {
    bucket = <span class="hljs-string">"danoph-file-uploader-demo-terraform"</span>
    key    = <span class="hljs-string">"terraform.tfstate"</span>
    region = <span class="hljs-string">"us-east-1"</span>
  }
}
</code></pre>
<p>You'll need to replace <code>danoph-file-uploader-demo-terraform</code> with the bucket you just created manually to house our terraform state.</p>
<p>Now, if you run <code>terraform init</code> you should see something similar to this output:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676662431730/1b86a0ac-9a0d-4819-a925-37f1fb237e7d.png" alt class="image--center mx-auto" /></p>
<p>We also want to make sure to add the <code>.terraform</code> folder to our <code>.gitignore</code>:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">'.terraform'</span> &gt;&gt; .gitignore
</code></pre>
<p>Now we can get onto the fun stuff. We need to create a few things in AWS for our app to work:</p>
<ul>
<li><p>An IAM user with an access key and secret that our app will use when it's deployed to Vercel.</p>
</li>
<li><p>An S3 bucket that we will use for our file uploads</p>
</li>
<li><p>Some permissions and policies around our app being able to work with our s3 bucket</p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># main.tf</span>

terraform {
  required_providers {
    aws = {
      <span class="hljs-built_in">source</span> = <span class="hljs-string">"hashicorp/aws"</span>
      version = <span class="hljs-string">"4.39.0"</span>
    }
  }
}

terraform {
  backend <span class="hljs-string">"s3"</span> {
    bucket = <span class="hljs-string">"danoph-file-uploader-demo-terraform"</span>
    key    = <span class="hljs-string">"terraform.tfstate"</span>
    region = <span class="hljs-string">"us-east-1"</span>
  }
}

locals {
  upload-bucket-name = <span class="hljs-string">"danoph-file-uploader-demo"</span>
}

resource <span class="hljs-string">"aws_s3_bucket"</span> <span class="hljs-string">"uploads"</span> {
  bucket = local.upload-bucket-name
}

resource <span class="hljs-string">"aws_s3_bucket_server_side_encryption_configuration"</span> <span class="hljs-string">"uploads"</span> {
  bucket = aws_s3_bucket.uploads.bucket

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = <span class="hljs-string">"AES256"</span>
    }
  }
}

resource <span class="hljs-string">"aws_s3_bucket_cors_configuration"</span> <span class="hljs-string">"uploads"</span> {
  bucket = aws_s3_bucket.uploads.id

  cors_rule {
    allowed_headers = [<span class="hljs-string">"*"</span>]
    allowed_methods = [<span class="hljs-string">"GET"</span>, <span class="hljs-string">"PUT"</span>, <span class="hljs-string">"POST"</span>]
    allowed_origins = [<span class="hljs-string">"*"</span>]
    expose_headers  = [<span class="hljs-string">"ETag"</span>]
  }
}

resource <span class="hljs-string">"aws_iam_user"</span> <span class="hljs-string">"vercel"</span> {
  name = <span class="hljs-string">"file-uploader-demo"</span>
  path = <span class="hljs-string">"/system/"</span>
}

resource <span class="hljs-string">"aws_iam_access_key"</span> <span class="hljs-string">"vercel"</span> {
  user = aws_iam_user.vercel.name
}

resource <span class="hljs-string">"aws_iam_policy"</span> <span class="hljs-string">"s3_access"</span> {
  name        = <span class="hljs-string">"vercel_file_uploader_demo"</span>
  path        = <span class="hljs-string">"/"</span>
  description = <span class="hljs-string">"IAM policy for s3 access from vercel"</span>

  policy = &lt;&lt;EOF
{
  <span class="hljs-string">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
  <span class="hljs-string">"Statement"</span>: [
    {
      <span class="hljs-string">"Action"</span>: [
        <span class="hljs-string">"s3:PutObject"</span>,
        <span class="hljs-string">"s3:GetObject"</span>,
        <span class="hljs-string">"s3:ListMultipartUploadParts"</span>
      ],
      <span class="hljs-string">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::<span class="hljs-variable">${local.upload-bucket-name}</span>/*"</span>,
      <span class="hljs-string">"Effect"</span>: <span class="hljs-string">"Allow"</span>
    }
  ]
}
EOF
}

resource <span class="hljs-string">"aws_iam_user_policy_attachment"</span> <span class="hljs-string">"s3_access"</span> {
  user       = aws_iam_user.vercel.name
  policy_arn = aws_iam_policy.s3_access.arn
}

output <span class="hljs-string">"access_key_id"</span> {
  value = aws_iam_access_key.vercel.id
}

output <span class="hljs-string">"access_key_secret"</span> {
  sensitive = <span class="hljs-literal">true</span>
  value = aws_iam_access_key.vercel.secret
}
</code></pre>
<p>To see what's going to be created in AWS, we can run the following command to do a dry run in terraform:</p>
<pre><code class="lang-bash">terraform plan
</code></pre>
<p>You will see the s3 bucket, IAM user and policy, etc. about to be created. When you're ready to make the changes, run:</p>
<pre><code class="lang-bash">terraform apply
</code></pre>
<p>It will ask you if you're sure you want to make these changes by typing yes. When I am using terraform in the real world, I make sure to review the plan before typing yes whenever I am making changes. Many times I have seen people not look closely enough at what terraform is about to do and they end up accidentally deleting a production database server, which can cause weeks worth of work if there aren't backups or redundancy set up. That's just a tip from me for your future terraform endeavors to review the plan before actually typing yes :)</p>
<p>Anyway, when the vercel IAM user gets created when running this terraform, make sure to copy the access key id somewhere safe. Since we marked the access_key_secret output as sensitive, we need to explicitly run this command to see the output value:</p>
<pre><code class="lang-bash">terraform output access_key_secret
</code></pre>
<p>Now that we have everything set up in AWS, let's switch over to Vercel and get everything set up over there. We will need to make sure our app is pushed up to GitHub first. After that, we can visit vercel and import our new repository:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676664060079/2401242a-1e4d-4b48-ac27-8c86979561e9.png" alt class="image--center mx-auto" /></p>
<p>Click on "Import" next to our repository. On the next page, we will need to add 4 environment variables that our <code>lib/s3.ts</code> file is referencing:</p>
<ol>
<li><p><code>bucket_region</code></p>
</li>
<li><p><code>upload_bucket</code></p>
</li>
<li><p><code>access_key_id</code></p>
</li>
<li><p><code>access_key_secret</code></p>
</li>
</ol>
<p>Vercel will automatically inject these values into our application that our application is referencing using <code>process.env</code>. This is good practice to avoid hardcoding secrets and passwords in applications. You'd be shocked to know how many projects I've seen that hardcode usernames and passwords directly into the codebase.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676664414778/80b65eac-4ca4-44f3-904a-8926db930695.png" alt class="image--center mx-auto" /></p>
<p>I purposely am not showing the values for <code>access_key_id</code> and <code>access_key_secret</code> since they are displayed in plain text. Just make sure to enter all 4 values and then click "Deploy"</p>
<p>After the successful deployment, you should be able to visit the domain vercel assigns your app:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676665041539/7df645b3-46ae-45d1-82ea-dab3e7a51418.png" alt class="image--center mx-auto" /></p>
<p>Once you visit the app, open up the developer console so you can see upload progress when uploading a file. Then click on the "Upload a file" button and watch the developer console for the upload to finish.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676665161950/2d2bad50-412c-408d-b488-024a85f825a5.png" alt class="image--center mx-auto" /></p>
<p>Now, if you head over to your S3 bucket in the AWS Console, you should see a new file has just arrived.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676665224470/d937d2f7-eb3a-4593-90ff-99e5f04062f1.png" alt class="image--center mx-auto" /></p>
<p>And there you have multipart chunked uploading directly to Amazon S3 from scratch!</p>
<p>If people are interested in knowing more about this topic, let me know in the comments. Since we aren't displaying the images on the page currently, we could walk through a way to display the files served through a CDN like CloudFront, or we could possibly explore expanding our single file uploader into a multiple file uploader.</p>
<p>The full source code for this application is here: <a target="_blank" href="https://github.com/danoph/file-uploader-demo">https://github.com/danoph/file-uploader-demo</a></p>
]]></content:encoded></item></channel></rss>