Serverless File Uploads with Supabase Storage and Next.js API Routes
 Mudasir Fayaz
Mudasir FayazHandling file uploads can often be a pain point for developers — especially when dealing with servers, storage permissions, and scaling. Luckily, with Supabase Storage and Next.js API Routes, you can build afully serverless upload system that’s secure, scalable, and easy to maintain.
Why Supabase Storage?
Supabase offers an S3-like storage system that integrates perfectly with your Supabase project and authentication layer. It supports:
- 📁 Organized file buckets with custom access policies
- 🔐 Built-in authentication and public/private file access
- ⚡ Direct uploads from serverless functions or the browser
“Supabase Storage gives you the power of cloud storage — without the AWS complexity.”
Step 1: Create a Supabase Project & Storage Bucket
Go to Supabase Dashboard, create a new project, and navigate to Storage → Buckets. Create a new bucket named uploads.
Then, go to the bucket’s Policies tab and enable public read if you want to allow direct access to uploaded files.
Step 2: Setup Next.js Project and Supabase Client
Create a new Next.js project and install the Supabase client library:
npx create-next-app@latest supabase-upload-demo
cd supabase-upload-demo
npm install @supabase/supabase-jsThen create a supabaseClient.ts file to connect your app with Supabase:
// lib/supabaseClient.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);Make sure to add your environment variables in a .env.local file.
Step 3: Building the File Upload API Route
We’ll use a serverless API route to handle uploads securely. This ensures your secret keys stay hidden and all uploads go through your backend.
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "@/lib/supabaseClient";
export async function POST(req: NextRequest) {
  try {
    const formData = await req.formData();
    const file = formData.get("file") as File;
    if (!file) return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
    const { data, error } = await supabase.storage
      .from("uploads")
      .upload(`public/${file.name}`, file, {
        cacheControl: "3600",
        upsert: false,
      });
    if (error) throw error;
    const { data: publicUrl } = supabase.storage
      .from("uploads")
      .getPublicUrl(`public/${file.name}`);
    return NextResponse.json({ url: publicUrl.publicUrl });
  } catch (err: any) {
    return NextResponse.json({ error: err.message }, { status: 500 });
  }
}This API route accepts a file via FormData, uploads it to Supabase Storage, and returns the public file URL.
Step 4: Creating the Upload Form
Now let’s create a simple frontend component that sends a file to the API route using a fetch request.
// components/FileUpload.tsx
'use client';
import { useState } from "react";
export default function FileUpload() {
  const [file, setFile] = useState<File | null>(null);
  const [url, setUrl] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  async function handleUpload() {
    if (!file) return;
    setLoading(true);
    const formData = new FormData();
    formData.append("file", file);
    const res = await fetch("/api/upload", { method: "POST", body: formData });
    const data = await res.json();
    setUrl(data.url);
    setLoading(false);
  }
  return (
    <div className="max-w-md mx-auto mt-10 bg-white p-6 rounded-xl shadow-md text-center">
      <h2 className="text-2xl font-semibold mb-4">Upload a File</h2>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] || null)}
        className="border w-full p-2 rounded-md mb-4"
      />
      <button
        onClick={handleUpload}
        disabled={loading}
        className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition"
      >
        {loading ? "Uploading..." : "Upload"}
      </button>
      {url && (
        <p className="mt-4 text-green-600">
          ✅ File uploaded:{" "}
          <a href={url} className="text-blue-600 underline" target="_blank">
            View File
          </a>
        </p>
      )}
    </div>
  );
}The component lets users select a file, sends it to your API route, and displays the public URL once the upload is complete.
Step 5: Securing Your Uploads
If you’re building a multi-user app, you can use Supabase authentication to upload files to user-specific folders (e.g., uploads/user-id/filename). You can also restrict read access by disabling public URLs and serving files through authenticated routes.
const user = await supabase.auth.getUser();
const path = `private/${user.id}/${file.name}`;Conclusion
With Supabase Storage and Next.js API Routes, building a serverless file upload system becomes effortless. You get a secure, scalable solution with zero server management — ideal for dashboards, SaaS tools, or user-generated content apps.
The best part? You can extend this setup with Supabase Auth, file previews, or image processing — all while staying within a simple, serverless architecture.