惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
量子位
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
U
Unit 42
IT之家
IT之家
F
Fortinet All Blogs
GbyAI
GbyAI
MongoDB | Blog
MongoDB | Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Register - Security
The Register - Security
NISL@THU
NISL@THU
Webroot Blog
Webroot Blog
A
Arctic Wolf
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
Recent Announcements
Recent Announcements
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Blog — PlanetScale
Blog — PlanetScale
L
LangChain Blog
P
Palo Alto Networks Blog
Y
Y Combinator Blog
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
AWS News Blog
AWS News Blog
有赞技术团队
有赞技术团队
Engineering at Meta
Engineering at Meta
C
Cybersecurity and Infrastructure Security Agency CISA
aimingoo的专栏
aimingoo的专栏
Know Your Adversary
Know Your Adversary
Cyberwarzone
Cyberwarzone
Martin Fowler
Martin Fowler
The Hacker News
The Hacker News
P
Privacy International News Feed
T
Threat Research - Cisco Blogs
G
GRAHAM CLULEY
宝玉的分享
宝玉的分享
博客园 - 聂微东
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The GitHub Blog
The GitHub Blog
S
Securelist
T
The Exploit Database - CXSecurity.com
T
Threatpost
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
F
Full Disclosure

ImageKit.io Blog

Next.js Image Optimization with ImageKit Use Video as a Background in Your Next.js Project How to Fix Autoplay Video in Next.js How Durian Scaled a Visual-First Retail Experience to 350K Monthly Visitors Online How Matsmart accelerated image delivery across countries with ImageKit AI in Digital Asset Management: From Smart Workflows to Agentic Automation How Joseph Joseph unified and secured global video delivery with ImageKit How Modall powers fast, effortless media delivery across 40+ projects with ImageKit Digital Asset Management (DAM) Trends: 2026 Report How to add a poster image to Video.js player (and automate it) HLS streaming with Video.js + React Building the future of storytelling with fast, AI-powered video delivery How PushOwl delivers 100M+ image-rich notifications seamlessly with ImageKit How Homify delivers millions of interior design images seamlessly with ImageKit Better event discovery with lightning‑fast videos & images Adding video player in React Native Video player in Angular applications Crop and resize videos in React React image and video upload React video optimization How we quadrupled our traffic to 625K monthly page views How Apollo 24|7 boosted performance & reduced costs with ImageKit Simplify your media workflows with ImageKit DAM integrations Extending Lighthouse for custom image and video optimization analysis Brand Asset Management: What is it? How does it work? WordPress Digital Asset Management Guide - Manage your WP media assets better Why Shopify retailers need a digital asset management solution DAM vs. SharePoint: Which is best for you? AI-powered Metadata and Tagging in Digital Asset Management How Hopscotch built India's largest online Kids' fashion brand with ImageKit Dropbox Vs. DAM: Which Is The Right Tool For Digital Asset Management Digital Asset Management for Photographers: A Complete Guide Why digital asset management for agencies is essential Helping both Top and Bottom Line: SaffronStays rapid, profitable growth with ImageKit How KreditBee simplified media experiences with ImageKit Google Drive alternatives for businesses (with fast-growing teams) Node.js image upload ImageKit: The Secret Ingredient in Swiggy’s Expansion Journey Streamlining the Design Approval Process: A Comprehensive Guide AV1 Codec - Complete guide for video application devs PHP image and video upload Angular image & video upload AV1 vs VP9: Which codec should you choose? Adding video player in Next.js React Video Player VP8 vs VP9 - In the context of online video delivery Exploring WebM vs MP4 7 Free Digital Asset Management Software that are not Open-Source Comparing 9 Top Digital Asset Management Tools in the Market What are Brand Standards and Why do they Matter? Boost Sales and Brand Appeal: Essential Tips for eCommerce Image Management Brand Recall: The Strategy to Create Unforgettable Brands How to upload files in HTML? Branding for Small Businesses (2025 Edition) Everything you need to know about VP9 codec Recent updates from ImageKit and what's next Best Ways to Write RFP For Digital Asset Management (+ with free RFP template) What is Brand Dilution? How to Avoid It? Explained with [Examples] The Importance of Brand Identity: Leveraging Digital Asset Management for Impact From Launch to Scale: How to Launch a Brand Campaign Digital Asset Management Requirements - What do You Need to Evaluate and How? Marketing Collateral Management: A Quick End-to-End Guide Video Content Management System: What Is It And How To Choose One? Dropbox vs. Google Drive vs. Onedrive: The Best Cloud Storage Solution How to Build Brand Trust: Get Started In 2025 Google Drive vs. Box: A Detailed Comparison How Digital Asset Management Solutions Help Protect Brand Equity A DAM Solution Can Safeguard Your Digital Intellectual Property - Here’s How WebP Vs. PNG: Which Image Format Should You Use and Why? How to Resize Images in Bootstrap Easily Progressive jpegs (PJPEG): the key to loading images faster on your website Dropbox vs. Google Drive: The Best Cloud Storage For Digital Assets Dropbox Pros & Cons In 2024: An In-Depth Analysis and Why A DAM Solution Stands Out Google Drive Vs OneDrive: The Better Storage Option For Digital Assets Manage your video assets better with video metadata Understanding DAM's Role in Strengthening Brand Identity Digital asset management strategy: What to know before creating one The Ultimate Guide To Marketing Agency Onboarding 6 Solutions To Simplify Large File Sharing Over The Web A Step-by-Step Breakdown of a Video Production Workflow 13 Digital Asset Management Use Cases You Should Know How to Conduct a Brand Audit and Manage Your Brand Assets Costly Consequences of Inconsistent Branding And How DAM Can Help Dynamic Asset Transformation: What It Is, Why You Need It, and How ImageKit Can Help Everything You Need to Know About HTML Video Autoplay How To Select Your DAM Vendor: A Complete Guide How to Boost User Experience with Smart Digital Asset Management React Image Optimization: A Guide for Web Developers Why Should DAM Be A Part Of Your MarTech Stack? Unleashing the Power of Content Repurposing with ImageKit MKV vs MP4: Which Video File Format Is Better for Your Needs? Digital Asset Management For Ecommerce: A Complete Guide How an Image Tagging Software can Transform Your Image Search How to Manage Your Content Lifecycle Effectively M4V vs MP4: Which Video Format Should You Use and Why? Why Every Business Needs An Image Management System All The Questions To Ask During A Dam Demo Which is the Best Image Format for Your Website? Uploading Multiple Files Using JavaScript: A Comprehensive Guide Headless DAM: Why API-Driven Digital Asset Management is the Way Forward
Next.js image and video upload
Jay Parekh · 2025-02-25 · via ImageKit.io Blog

In this guide, we will learn step-by-step how to handle file uploads in a Next.js application.

You can check out the final, working application here, or explore the source code from this GitHub repository.

The final result will look like this:

This guide is divided into the following sections:

  • Set up the Next.js application
  • Add a simple upload button
  • Set up the backend to accept and save assets
  • Add basic validation and security
  • Add some bells and whistles (drag-and-drop, copy-paste, progress bar)
  • Why this upload module is not efficient or scalable
  • Set up direct uploads from browser to ImageKit

Requirements

To complete this tutorial, we will need the following. If you are using more recent versions of any of these components, you may have to make minor adjustments. The guide also assumes you have a basic knowledge of HTML, JavaScript, and CSS.

  1. Node.js - version 22
  2. npm
  3. npx (comes packaged with npm; if not, then install with npm install -g npx)

Set up the Next.js application

Let’s set up a starter Next.js application.

Run the following command in the directory that you want the project to be created:

npx create-next-app

You will be asked a few questions related to the configuration of the project. You may answer them as you prefer.

Now, run

npm run dev

When we navigate to localhost:3000 in our browser, we should see a default home page like this:

Next.js default starter page
Next.js default starter page

Add a simple upload button

Let’s remove everything from src/app/page.js, and begin building our upload interface. We start by adding a basic <input> element to accept files.

src/app/page.js should have the following code:

'use client' 

import styles from "./page.module.css";

export default function Home() {
  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <input type="file" />
      </main>
    </div>
  );
}

The webpage at localhost:3000 should now look like this:

The webpage now has a basic upload HTML element
The webpage now has a basic upload HTML element

Now, when we click on “Browse…“ and select a file to upload, the file does get selected with its name displayed in place of “Browse…“, but we are not sending the file anywhere outside of this webpage.

Let’s fix that by adding a JavaScript “handler“ function to do something with the picked-up file. The handler function  handleUploadsends a POST request to the route /api/upload with the file attached to the request body. We use the native FormData API to simplify this.

src/app/page.js should have the following code:

'use client';
import styles from "./page.module.css";
 
export default function Home() {
  const handleUpload = (file) => {
    if (!file) return;
 
    const formData = new FormData();
    formData.append('file', file);
 
    const xhr = new XMLHttpRequest();
 
    xhr.onload = (obj) => {
      if (xhr.status === 200) {
        alert("File uploaded successfully!")
      } else {
        alert("File could not be uploaded!")
      }
    };
 
    xhr.onerror = () => {
      alert("File could not be uploaded!")
    };
 
    xhr.open('POST', '/api/upload');
    xhr.send(formData);
  };
 
  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
      </main>
    </div>
  );
}

Set up the backend to accept and save assets

The handleUpload function attempts to send the file to the /api/upload API, but there is no such API endpoint. Let’s create one. Create a new file src/app/api/upload/route.js

src/app/api/upload/route.js should have the following code:

import { writeFile, mkdir } from 'fs/promises';
import { NextResponse } from 'next/server';
import path from 'path';
 
export async function POST(request) {
  try {
    const formData = await request.formData();
    const file = formData.get('file');
     
    // error out if no file is received via FormData
    if (!file) {
      return NextResponse.json(
        { error: 'No file received.' },
        { status: 400 }
      );
    }
 
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
 
    // build the complete path to our 'public/uploads' directory
    const filePath = path.join(process.cwd(), 'public/uploads', file.name);
    // ensure the uploads directory exists
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(filePath, buffer);
 
    return NextResponse.json({ message: 'File uploaded successfully!' });
  } catch (error) {
    // handle any unknown error
    return NextResponse.json(
      { error: 'Error uploading file.' },
      { status: 500 }
    );
  }
}

We now have an API route at /api/upload that accepts a file called file in the request body, and saves it to the public/uploads/ directory in the project. Let’s try it out. Click on “Browse…“ on the webpage again, and select a file. You should see the “File uploaded successfully“ browser alert popup. In the public/uploads directory of your project, you should see the uploaded file.

Add basic validation and security

However, the request might fail if your selected file is larger than the default limit that Next.js sets up. Let’s fix that, and allow files up to a maximum of 5 MB (or some other number) to be uploaded. Along with this, we also add a few basic validation checks on the incoming file. This is critical to protect your server from attacks via malicious file uploads. We will revisit the security aspect of file uploads in a later section. Let’s add our validations now.

Make the following changes in src/app/api/upload/route.js:

// old imports
export async function POST(request) {
  try {
    // old code
 
    // Validate file type
    const allowedTypes = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'image/webp',
      'video/mp4',
      'video/webm',
      'video/quicktime'
    ];
 
    if (!allowedTypes.includes(file.type)) {
      return NextResponse.json(
        { error: 'File type not allowed. Only images and videos are accepted.' },
        { status: 400 }
      );
    }
 
    // Validate file size (5MB = 5 * 1024 * 1024 bytes)
    const maxSize = 5 * 1024 * 1024;
    if (file.size > maxSize) {
      return NextResponse.json(
        { error: 'File size exceeds 5MB limit.' },
        { status: 400 }
      );
    }
 
    // Validate filename
    const filename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
    if (filename !== file.name) {
      return NextResponse.json(
        { error: 'Filename contains invalid characters.' },
        { status: 400 }
      );
    }
 
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
 
    const uploadDir = path.join(process.cwd(), 'public/uploads');
    await mkdir(uploadDir, { recursive: true });
    await writeFile(path.join(uploadDir, filename), buffer);
 
    return NextResponse.json({
      message: 'File uploaded successfully!',
      filename: filename
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Error uploading file.' },
      { status: 500 }
    );
  }
}

You should see an error being raised like this on invalid file uploads:

We have successfully created our basic file upload module!

Add some bells and whistles (drag-and-drop, copy-paste, progress bar)

Now, it’s time to make it look pretty. We will add three things:

  1. A progress bar for an ongoing upload
  2. The ability to drag and drop files for upload
  3. The ability to paste a file from the clipboard for upload

1. Progress bar

To display a progress bar, we use the onprogress event handler on the XMLHttpRequest.upload object. The event object received as an argument to this callback gives us two key numbers:

1. The number of bytes sent to the server

2. The total number of bytes that need to be sent to the server

Using these two numbers, we calculate the progress percentage of our upload. Let’s write the code for this.

Along with adding a progress bar, we apply some cosmetic changes as well.

Make the following changes to src/app/page.js:

// old imports
import { useState, useRef } from "react"

export default function Home() {
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadStatus, setUploadStatus] = useState(null);
  const [uploadStats, setUploadStats] = useState({ loaded: 0, total: 0 });
  const [isUploading, setIsUploading] = useState(false);
  
  const resetUpload = () => {
    setUploadProgress(0);
    setUploadStatus(null);
    setUploadStats({ loaded: 0, total: 0 });
    setIsUploading(false);
    if (formRef.current) {
      formRef.current.reset();
    }
  };

  // modified upload handler
  const handleUpload = (file) => {
    // old code

    setIsUploading(true);
    setUploadStatus(null);

    // new event callback
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const progress = (event.loaded / event.total) * 100;
        setUploadProgress(progress);
        setUploadStats({
          loaded: event.loaded,
          total: event.total
        });
      }
    };

    // set the upload progress to 0 after request completion
    xhr.onload = (obj) => {
      setIsUploading(false);
      if (xhr.status === 200) {
        setUploadStatus('success');
        setUploadProgress(100);
      } else {
        setUploadStatus('error');
        setUploadProgress(0);
      }
    };

    // set the upload progress to 0 after request failure too
    xhr.onerror = () => {
      setUploadStatus('error');
      setUploadProgress(0);
      setIsUploading(false);
    };

    // old code
  };

  const formatBytes = (bytes) => {
    if (bytes === 0) return '0 KB';
    const k = 1024;
    return `${(bytes / k).toFixed(1)} KB`;
  };

  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <div className={`${styles.uploadArea} ${isUploading ? styles.disabled : ''} ${(uploadProgress > 0 || uploadStatus) ? styles.withProgress : ''}`}>
          <p className={styles.uploadText}>
            Select an image or video to upload<br />
          </p>
          <label className={`${styles.fileInputLabel} ${isUploading ? styles.disabled : ''}`}>
            <input
              type="file" 
              name="file"
              className={styles.fileInput}
              disabled={isUploading}
              onChange={(e) => handleUpload(e.target.files[0])}
            />
            Choose a file
          </label>

          {(uploadProgress > 0 || uploadStatus) && (
            <div className={styles.uploadProgress}>
              {uploadStatus ? (
                <div className={`${styles.uploadStatus} ${styles[uploadStatus]}`}>
                  {uploadStatus === 'success' ? (
                    <>
                      <p>✓ Upload completed successfully!</p>
                      <button type="button" className={styles.restartButton} onClick={resetUpload}>
                        ↺ Upload another file
                      </button>
                    </>
                  ) : (
                    <>
                      <p>✕ Upload failed. Please try again.</p>
                      <button type="button" className={styles.restartButton} onClick={resetUpload}>
                        ↺ Try again
                      </button>
                    </>
                  )}
                </div>
              ) : (
                <>
                  <div className={styles.uploadProgressHeader}>
                    <div className={styles.fileIcon}>📄</div>
                    <div>Uploading...</div>
                  </div>
                  <div className={styles.progressContainer}>
                    <div 
                      className={styles.progressBar} 
                      style={​{ width: `${uploadProgress}%` }​}
                    />
                  </div>
                  <div className={styles.progressStats}>
                    <span>{formatBytes(uploadStats.loaded)} / {formatBytes(uploadStats.total)}</span>
                    <span>{Math.round(uploadProgress)}%</span>
                  </div>
                </>
              )}
            </div>
          )}
        </div>
      </main>
    </div>
  );
}

We also need some CSS to make the progress bar look nice. Add the following CSS to the end of src/app/page.module.css

.progressContainer {
  width: 100%;
  margin-bottom: 1rem;
  background: rgba(0, 112, 243, 0.08);
  border-radius: 4px;
  overflow: hidden;
}

.progressBar {
  height: 6px;
  background: linear-gradient(90deg, #0070f3, #00a3ff);
  transition: width 0.3s ease-out;
  border-radius: 3px;
}

.uploadProgress {
  border: 1px solid rgba(0, 112, 243, 0.1);
  border-radius: 8px;
  padding: 1rem;
  margin-top: 1rem;
  background: rgba(0, 112, 243, 0.03);
  backdrop-filter: blur(8px);
}

.uploadProgressHeader {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 0.75rem;
  color: #666;
  font-size: 0.9rem;
}

.fileIcon {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 112, 243, 0.08);
  border-radius: 6px;
  color: #0070f3;
  font-size: 1rem;
}

.progressStats {
  display: flex;
  justify-content: space-between;
  font-size: 0.875rem;
  color: #666;
  margin-top: 0.5rem;
}

.uploadStatus {
  text-align: center;
  padding: 1rem;
}

.uploadStatus.success {
  color: #16a34a;
}

.uploadStatus.error {
  color: #dc2626;
}

.restartButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  background: #f5f5f5;
  color: #666;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.875rem;
  margin-top: 1rem;
  transition: all 0.2s ease;
}

.restartButton:hover {
  background: #e5e5e5;
}

@media (prefers-color-scheme: dark) {
  .uploadProgress {
    border-color: rgba(0, 112, 243, 0.2);
    background: rgba(0, 112, 243, 0.05);
  }

  .fileIcon {
    background: rgba(0, 112, 243, 0.15);
    color: #3291ff;
  }

  .progressContainer {
    background: rgba(0, 112, 243, 0.15);
  }

  .uploadProgressHeader {
    color: #999;
  }

  .restartButton {
    background: #262626;
    color: #999;
  }

  .restartButton:hover {
    background: #333;
  }
}

.uploadArea {
  padding: 2rem;
  border: 2px dashed #ccc;
  border-radius: 8px;
  text-align: center;
  background: #fafafa;
  transition: all 0.2s ease;
  cursor: pointer;
  width: 100%;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.5rem;
}

.uploadArea.withProgress {
  min-height: 320px;
  transition: min-height 0.3s ease-out;
}

.uploadArea.disabled {
  opacity: 0.7;
  cursor: not-allowed;
}

.uploadArea.disabled:hover {
  border-color: #ccc;
  background: #fafafa;
}

.uploadText {
  margin: 0;
  color: #666;
}

.uploadText span {
  display: block;
  margin: 0.25rem 0;
  color: #999;
}

.fileInput {
  display: none;
}

.fileInputLabel {
  display: inline-block;
  padding: 0.5rem 1rem;
  background: #0070f3;
  color: white;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s ease;
}

.fileInputLabel:hover {
  background: #0060df;
}

.fileInputLabel.disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: #999;
}

.fileInputLabel.disabled:hover {
  background: #999;
}

File uploads will now show a progress bar. However, the progression in the progress bar may not be noticeable. This is because the upload begins and completes almost instantly since we are using a local backend to handle the upload. You can use the throttling option in the “Network” tab of the developer tools in your browser. Setting the throttling level to “Good 3G“ or “Regular 3G“ (on Firefox) does the trick for me. The upload is slowed down now and the progress bar takes its time to fill up.

Here’s what it looks like:

The progress bar in action
The progress bar in action

2. Drag and Drop

Let’s add the ability to drag and drop a file into the page to initiate the upload. Achieving this is rather simple without the need for an external package. We use the native HTML onDragOver, onDragLeave, and onDrop events to enable this feature.

src/app/page.js should have the following code:

// old imports

export default function Home() {
  // old state definitions
  const [isDragging, setIsDragging] = useState(false);

  // old code
  // use a designated callback for the vanilla HTML <input> element
  const handleFileInput = (e) => {
    const file = e.target.files[0];
    handleUpload(file);
  };
 
  // new handlers for drag-and-drop
  const handleDragOver = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  };
 
  const handleDragLeave = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  };
 
  const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
 
    const file = e.dataTransfer.files[0];
    handleUpload(file);
  };

  return (
    // attach the drag-and-drop handlers to the main, outermost div
    <div 
      className={styles.page}
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
    > 
      // change the main element to incorporate drag-and-drop
      {isDragging && !isUploading && (
        <div className={styles.dropZone}>
          <div className={styles.dropZoneContent}>
            Drop your file here
          </div>
        </div>
      )}
      <main className={styles.main}>
        <div className={styles.uploadArea}>
          <p className={styles.uploadText}>
            Drag and drop your file here<br />
            <span>or</span>
          </p>
          <label className={styles.fileInputLabel}>
            <input 
              type="file" 
              onChange={(e) => handleUpload(e.target.files[0])}
              className={styles.fileInput} 
            />
            Choose a file
          </label>

          // old code
    </div>
  );
}

We need to add some styling too to make the UI of the drag-and-drop look intuitive.

Add the following classes to src/app/page.module.css:

.dropZoneContent {
  padding: 2rem 4rem;
  background: white;
  border: 2px dashed #0070f3;
  border-radius: 8px;
  font-size: 1.25rem;
  color: #0070f3;
}

.dropZone {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 112, 243, 0.05);
  backdrop-filter: blur(2px);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
}

The page should look like this now:

Upload via drag-and-drop
Upload via drag-and-drop

3. Paste from clipboard

Let’s add a third method to upload files on the page - Pasting a file from the clipboard. Most modern browsers and operating systems support pasting files into a webpage using the paste shortcut (ctrl/cmd + v). But our webpage needs to have the JavaScript code to detect the paste and handle the file. Let’s add that code!

src/app/page.js should have the following code:

// old imports
import { useEffect, useState } from "react";

export default function Home() {
  // old state definitions

  // old code
  // attach an event listener for the "paste" event. capture and upload the file in this handler
  useEffect(() => {
    const handlePaste = (e) => {
      e.preventDefault();
      const items = e.clipboardData?.items;
      
      if (!items) return;

      for (const item of items) {
        if (item.kind === 'file') {
          const file = item.getAsFile();
          handleUpload(file);
          break;
        }
      }
    };

    window.addEventListener('paste', handlePaste);

    return () => {
      window.removeEventListener('paste', handlePaste);
    };
  }, []);

  return (
      // old code
      <main className={styles.main}>
        <div className={`${styles.uploadArea} ${isUploading ? styles.disabled : ''} ${(uploadProgress > 0 || uploadStatus) ? styles.withProgress : ''}`}>
          <p className={styles.uploadText}>
            Drag and drop your file here<br />
            <span>or</span>
          </p>        
          // add some text to let the user know they can paste files for uploading
          <p className={styles.uploadHint}>
            You can also paste files using Ctrl+V / Cmd+V
          </p>
          <label className={styles.fileInputLabel}>
        
            // old code
            
          </label>
        </div>
      </main>
      // old code
  );
}

Add the following class to src/app/page.module.css

.uploadHint {
  margin: 0.5rem 0;
  font-size: 0.9rem;
  color: #666;
  font-style: italic;
}

The webpage should now look like this, and pasting a copied file should successfully upload it.

The complete upload interface
The complete upload interface

Why this upload module is not efficient or scalable

We are currently storing files locally, but this won't work if our application becomes popular. We need a solution that can handle millions of files, terabytes of storage, and hundreds of requests per second. The problem is even bigger with videos since they are large files and take a long time to upload. If our users are from different parts of the world, we need to run our backend in multiple regions to ensure a good user experience. While a DIY approach is good for learning, it isn't practical for real-world applications.

Let's look at some common problems and explore solutions for them:

Limited File Storage on Servers

  • Files are stored on a file system, and they have limited disk space. Scaling up requires increasingly larger disks.
  • Local disks can only be attached to one server, preventing multiple application instances.
  • What we need is a globally distributed upload backend that can manage billions of files, terabytes of storage, and hundreds of requests per second.

High Latency with Large File Uploads

  • Large files like videos take longer to upload.
  • Users in different regions may experience high latencies when uploading to a single region.
  • We can deploy and run the upload backend in multiple regions to solve this.

Centralization of the file store

  • In our DIY setup, the uploaded files are stored at a single, centralized location. If you have multiple instances of your upload API servers running, they must all upload to this same central store.
  • This becomes a bottleneck for performance, besides being a single point of failure.
  • We need the flexibility of being able to use multiple storage systems. Using a service like AWS S3 for the actual file storage makes it a lot more reliable.

File Delivery

  • After uploading, delivering files to users requires another application layer.
  • We need an application that sits in front of the storage, scales automatically, handles very high request rates, and has caching integrated in it so we don’t overburden our storage system.

Security Concerns

  • File uploads can be entry points for hackers to inject malicious files. This could compromise the entire cloud infrastructure.
  • We need to decouple the file upload, storage, and delivery systems from our main application containing the business logic.

ImageKit.io is a third-party service that can handle all your file upload and storage needs, solving each of the above problems. Let’s see how:

  • File Storage: ImageKit can handle billions of files, terabytes of storage, and hundreds of requests per second.
  • Latency: ImageKit offers a global upload API endpoint. Upload requests are routed to the nearest of seven strategically located regions to minimize latency.
  • Decentralization: The upload endpoint is highly available and distributed so that the file upload does not become a point of failure for your application. Moreover, ImageKit allows you to use multiple storage services for the actual file storage.
  • Delivery: ImageKit provides a robust delivery API and supports high request rates with multiple layers of caching. It offers features such as intelligent image optimization, resizing, cropping, watermarking, file format conversion, video optimization, and streaming.
  • Security: With ImageKit, you offload file handling to an external service to protect your servers from attacks. This allows you to decouple upload, storage, and delivery from the rest of your application.

Let’s integrate ImageKit into our upload module.

Set up direct uploads from browser to ImageKit

Let's do everything again, but this time using ImageKit with a few lines of code. The overall functionality will remain unchanged. To ease the development, we will use ImageKit Next.js SDK. In this case, we will upload files to ImageKit directly from the browser.

With this integration, our frontend upload interface remains unchanged. Our backend upload API becomes completely redundant. The webpage will now send the file directly to ImageKit’s public upload endpoint, authenticated by your ImageKit API key.

Set up the ImageKit Next.js SDK

Installing the ImageKit Next.js SDK in our app is pretty simple:

npm install --save imagekitio-next imagekit

Before adding ImageKit-related code to the project, let’s split our website into two different routes. One for native uploads that we have built so far, and another for ImageKit uploads that we are about to build. Create two new files src/app/native-upload/page.js, and src/app/imagekit-upload/page.js. Move everything currently in src/app/page.js into src/app/native-upload/page.js, and overwrite the src/app/page.js file with the following code. Don’t forget to update the import path of the CSS file to ../page.module.css.

'use client';
import styles from "./page.module.css";
import Link from 'next/link';

export default function Home() {
  return (
    <div className={styles.homePage}>
      <div className={styles.demoContainer}>
        <h1>Next.js upload file demo</h1>
        <Link href="/native-upload" className={styles.demoLink}>
          Native Next.js upload demo
        </Link>
        <Link href="/imagekit-upload" className={styles.demoLink}>
          ImageKit Next.js upload demo
        </Link>
      </div>
    </div>
  );
}

Also, add the following new styles to src/app/page.module.css

.demoContainer {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.demoContainer h1 {
  margin-bottom: 2rem;
  text-align: center;
  color: #000033;
}

.demoLink {
  display: block;
  padding: 1rem;
  margin: 1rem 0;
  border: 2px dashed #4285f4;
  border-radius: 4px;
  text-align: center;
  color: #4285f4;
  text-decoration: none;
  transition: all 0.3s ease;
}

.demoLink:hover {
  background: #4285f4;
  color: white;
}

.homePage {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

Initialize the Next.js SDK:

Before using the SDK, let's learn how to obtain the necessary initialization parameters:

  • urlEndpoint is a required parameter. This can be obtained from the URL-endpoint section or the developer section on your ImageKit dashboard.
  • publicKey and authenticator parameters are needed for client-side file upload. publicKey can be obtained from the Developer section on your ImageKit dashboard.
  • authenticator expects an asynchronous function that resolves with an object containing the necessary security parameters i.e signature, token, and expire. We will see how to generate these in the following section.

💡

Note: Do not include your private key in any client-side code.

To integrate uploads to ImageKit in our application, we will need to define and add a bunch of new variables and functions. Let’s add them and understand why each change is necessary.

Overwrite the code inside src/app/imagekit-upload/page.js with the following:

'use client';

import { ImageKitProvider, IKUpload } from "imagekitio-next";
import styles from "../page.module.css";
import { useState, useEffect, useRef, useCallback } from "react";


export default function Home() {
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadStatus, setUploadStatus] = useState(null);
  const [uploadStats, setUploadStats] = useState({ loaded: 0, total: 0 });
  const [isUploading, setIsUploading] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const ikUploadRef = useRef(null)

  // This function fires a "change" event on the IKUpload's internal <input> element
  const uploadViaIkSdk = useCallback((files) => {
    if (ikUploadRef?.current) {
      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype,
        'files').set;
      nativeInputValueSetter.call(ikUploadRef.current, files);
      const changeEvent = new Event('change', { bubbles: true });
      ikUploadRef.current.dispatchEvent(changeEvent);
    }
  }, [ikUploadRef])

  // Call our backend API to generate short-lived authentication credentials using our ImageKit API key
  const authenticator = async () => {
    try {
      const response = await fetch("/api/auth");
  
      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Request failed with status ${response.status}: ${errorText}`);
      }
  
      const data = await response.json();
      const { signature, expire, token } = data;
      return { signature, expire, token };
    } catch (error) {
      throw new Error(`Authentication request failed: ${error.message}`);
    }
  };

  // This function now calls 'uploadViaSdk' to trigger the "change" event on IKUpload
  useEffect(() => {
    const handlePaste = (e) => {
      e.preventDefault();
      const files = e.clipboardData?.files;
      
      if (!files || files.length === 0) return;

      uploadViaIkSdk(files)
    };

    window.addEventListener('paste', handlePaste);

    return () => {
      window.removeEventListener('paste', handlePaste);
    };
  }, []);

  // event handlers for IKUpload: onError, onProgress, and onSuccess
  const onError = (err) => {
    setUploadStatus('error');
    setUploadProgress(0);
    setIsUploading(false);
  };
  
  const onProgress = (e) => {
    if (e.lengthComputable) {
      const progress = (e.loaded / e.total) * 100;
      setUploadProgress(progress);
      setUploadStats({
        loaded: e.loaded,
        total: e.total
      });
    }
  };

  const onSuccess = (res) => {
    setIsUploading(false);
    setUploadStatus('success');
    setUploadProgress(100);
  };

  const resetUpload = () => {
    setUploadProgress(0);
    setUploadStatus(null);
    setIsUploading(false);
  };

  const formatBytes = (bytes) => {
    if (bytes === 0) return '0 KB';
    const k = 1024;
    return `${(bytes / k).toFixed(1)} KB`;
  };

  const handleDragOver = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  };

  const handleDragLeave = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  };

  // This function now calls 'uploadViaSdk' to trigger the "change" event on IKUpload
  const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);

    uploadViaIkSdk(e.dataTransfer.files)
  };

  return (
    // The main <div> is wrapped under this provider to make ImageKit-related variables available
    <ImageKitProvider
      publicKey={process.env.NEXT_PUBLIC_PUBLIC_KEY}
      urlEndpoint={process.env.NEXT_PUBLIC_URL_ENDPOINT}
      authenticator={authenticator} 
    >
      {/* The <IKUpload> is internally simply an <input> file picker. But since we have our own three upload UI interfaces, ...
      ... we hide the <IKUpload> element, and just reference it to manually trigger a “change“ event on it. */}
      <IKUpload
        onError={onError}
        onSuccess={onSuccess}
        onUploadProgress={onProgress}
        // we use this ref to manually trigger the "change" event on this element
        ref={ikUploadRef}
        style={​{visibility: 'hidden', height: 0, width: 0}​} // hide the default button
      />
      <div 
        className={styles.page}
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
      >
        {ikUploadRef && (
          <>
            {isDragging && !isUploading && (
              <div className={styles.dropZone}>
                <div className={styles.dropZoneContent}>
                  Drop your file here
                </div>
              </div>
            )}
            <main className={styles.main}>
              <div className={`${styles.uploadArea} ${isUploading ? styles.disabled : ''} ${(uploadProgress > 0 || uploadStatus) ? styles.withProgress : ''}`}>
                <p className={styles.uploadText}>
                  Drag and drop your file here<br />
                  <span>or</span>
                </p>
                <p className={styles.uploadHint}>
                  You can also paste files using Ctrl+V / Cmd+V
                </p>
                <label className={`${styles.fileInputLabel} ${isUploading ? styles.disabled : ''}`}>
                  <input
                    type="file" 
                    name="file"
                    className={styles.fileInput}
                    disabled={isUploading}
                    // This function now calls 'uploadViaSdk' to trigger the "change" event on IKUpload
                    onChange={(e) => {
                      e.stopPropagation()
                      e.preventDefault()
                      uploadViaIkSdk(e.target.files)
                    }​}
                  />
                  Choose a file
                </label>
                
                {(uploadProgress > 0 || uploadStatus) && (
                  <div className={styles.uploadProgress}>
                    {uploadStatus ? (
                      <div className={`${styles.uploadStatus} ${styles[uploadStatus]}`}>
                        {uploadStatus === 'success' ? (
                          <>
                            <p>✓ Upload completed successfully!</p>
                            <button type="button" className={styles.restartButton} onClick={resetUpload}>
                              ↺ Upload another file
                            </button>
                          </>
                        ) : (
                          <>
                            <p>✕ Upload failed. Please try again.</p>
                            <button type="button" className={styles.restartButton} onClick={resetUpload}>
                              ↺ Try again
                            </button>
                          </>
                        )}
                      </div>
                    ) : (
                      <>
                        <div className={styles.uploadProgressHeader}>
                          <div className={styles.fileIcon}>📄</div>
                          <div>Uploading...</div>
                        </div>
                        <div className={styles.progressContainer}>
                          <div 
                            className={styles.progressBar} 
                            style={​{ width: `${uploadProgress}%` }​}
                          />
                        </div>
                        <div className={styles.progressStats}>
                          <span>{formatBytes(uploadStats.loaded)} / {formatBytes(uploadStats.total)}</span>
                          <span>{Math.round(uploadProgress)}%</span>
                        </div>
                      </>
                    )}
                  </div>
                )}
              </div>
            </main>
          </>
        )}
      </div>
    </ImageKitProvider>
  );
}

Create a file .env at the root of your project and add the following to it:

NEXT_PUBLIC_PUBLIC_KEY=<your_imagekit_public_key>
NEXT_PUBLIC_URL_ENDPOINT=<your_imagekit_url_endpoint>
PRIVATE_KEY=<your_imagekit_private_key>

Create a file src/app/api/auth/route.js and add the following to it:

import ImageKit from "imagekit";
import { NextResponse } from "next/server";

const imagekit = new ImageKit({
  publicKey: process.env.NEXT_PUBLIC_PUBLIC_KEY,
  privateKey: process.env.PRIVATE_KEY,
  urlEndpoint: process.env.NEXT_PUBLIC_URL_ENDPOINT,
});

export async function GET(request) {
  const authParams = imagekit.getAuthenticationParameters()
  return NextResponse.json(authParams);
}

Let’s describe all of the changes and the reason for making them:

  • The <div> in the return statement is now wrapped inside a <ImageKitProvider>, which sets up the context with required ImageKit-related variables.
  • The <IKUpload> is internally simply an <input> file picker. But since we have our own three upload UI interfaces, we hide the <IKUpload> element and just reference it to manually trigger a “change“ event on it.
  • ikUploadRef is used to reference and control the <IKUpload> element. This is required to manually trigger the “change“ event on this element.
  • The authenticator function calls a backend API (which we will write next) to generate authentication parameters for the file upload. This is required because you can authenticate to ImageKit only via your account private key, and we never store our private key in the frontend code.
  • The uploadViaIkSdk function fires a “change” event on the <IKUpload> element. The “change” event triggers the HTTP requests for authentication and finally, the actual upload. This is required because we have three different UI handlers to initiate uploads, and they all need a common interface to trigger the <IKUpload> element’s change handler.
  • onSuccess, onError, onProgress are callbacks to handle the respective events received from <IKUpload>
  • handleDrop now calls uploadViaIkSdk instead of the previous handleFileInput, which has been removed completely now.
  • The “paste“ event handler now calls uploadViaIkSdk instead of the previous handleFileInput, which has been removed completely.
  • The onChange event handler on the <input> element now also calls uploadViaIkSdk instead of handleFileInput

Here’s how the flow of our uploads works now:

Flow of upload
Flow of upload
  1. When a file is sent for upload via any of the three methods, a “change“ event on the <IKUpload> element is fired.
  2. The “change“ event internally triggers two HTTP requests. One to our own application’s backend to generate the authentication parameters, and another to ImageKit’s public upload endpoint for the actual upload.
  3. The upload finishes successfully (or fails) and the corresponding callbacks that we sent to <IKUpload> are invoked.

Following these changes, the visual output on the webpage should remain exactly the same. The only change is in the upload destination. The files are now uploaded to your ImageKit account instead of your local project directory.

Validation

When uploading to ImageKit, we have the option of writing validation checks both for the frontend and the backend. Backend checks are much more secure than frontend checks since they are executed on your application server. Let’s see how to implement both in our upload module via ImageKit.

  1. Backend checks: The <IKUpload> component accepts a checks prop that has a string value. This string represents what server-side checks must be performed before the file is uploaded. Learn about upload checks in detail here. For example, "request.folder : "offsites/" will limit file uploads to the folder offsites and its sub-folders.
  2. Frontend checks: The <IKUpload> component accepts a function for the prop validateFile. Let’s utilize that to run a basic frontend file validation check.

The corresponding code changes in src/app/imagekit-upload/page.js for this are as follows:

// define two state variables to track frontend validation errors
const [isFileValid, setIsFileValid] = useState(true)
const [fileValidationError, setFileValidationError] = useState(undefined)

// code to run the validations
const validateFile = (file) => {
  if (file?.size > 5 * 1024 * 1024) {
    setIsFileValid(false)
    setFileValidationError("File must be less than 5MB in size.")
    return false
  }
  if (!file?.type?.startsWith("image/") && !file?.type?.startsWith("video/")) {
    setIsFileValid(false)
    setFileValidationError("File must be an image or a video.")
    return false
  }
  setIsFileValid(true)
  setFileValidationError(undefined)
  return true
}

// show an alert if validation fails
useEffect(() => {
  if (!isFileValid) {
    alert(fileValidationError ?? "File is not valid. Please make sure it is an image or a video, and less than 5MB in size.")
    setIsFileValid(true)
    setFileValidationError(undefined)
  }
}, [isFileValid, fileValidationError])

// Assign the `validateFile` and `checks` props to IKUpload
<IKUpload
  ...
  validateFile={validateFile}
  checks={'"request.folder":"offsites/"'}  ...
>

Bonus: Optimize, transform, and deliver the uploaded image/video

Now that you have safely uploaded your image or video to ImageKit, let’s see how ImageKit helps with the file’s delivery flow. ImageKit can do three (among many other) things when delivering your image or video to your frontend clients:

  • Optimize: ImageKit will convert the image or video into the format that has the lowest file size before delivering it.
  • Transform: You can perform a host of transformations on the image/video like resizing, cropping, rotating, and a lot more.
  • Cache: ImageKit caches all your assets on the CDN by default. Even the transformed and optimized versions of them!

Here’s some Next.js code to do all of the above things:

/**
* This fetches an image from "https://ik.imagekit.io/<your_imagekit_id>/<your_url_endpoint_identifier>/default-image.jpg",
* optimizes it to get the lowest possible file size, applies the resize and rotation transformations,
* caches it at the CDN, and finally delivers it to this webpage.
** /
<ImageKitProvider urlEndpoint="https://ik.imagekit.io/<your_imagekit_id>/<your_url_endpoint_identifier>/"
>
  <IKImage 
    path="/default-image.jpg"
    transformation={[
      {
        "height": "200",
        "width": "200",
      },
      {
        "rotation": "90"
      }
    ]}
    alt="Alt text"
  />
</ImageKitProvider>

Conclusion

In this tutorial, we've covered:

  • Building a file upload application in Next.js from scratch.
  • Implementing file type and size validations for secure uploads.
  • Transitioning to ImageKit.io for direct uploads from the browser, making our application more efficient and scalable.
  • Utilizing ImageKit's free upload API and Next.js SDK for a streamlined experience with minimal code.

You can check out the live demo on CodeSandbox and explore the code on Github.