Next Starter Kit
Features

File Uploads

Secure file uploads with UploadThing integration

File Uploads

The starter kit uses UploadThing for secure, authenticated file uploads.

Features

  • Authenticated Uploads - Only signed-in users can upload
  • File Type Validation - Restrict by MIME type
  • Size Limits - Configurable max file size
  • Image Optimization - Automatic image compression
  • CDN Delivery - Fast global content delivery
  • User Tracking - Audit who uploaded what
  • Multiple Upload Endpoints - Different limits per use case

Setup

1. Create UploadThing Account

Sign up at uploadthing.com.

2. Get API Token

  1. Go to Dashboard
  2. Create an app
  3. Copy the token

3. Configure Environment

UPLOADTHING_TOKEN="eyJhcHBJZCI6xxxxxxxxxxxx"

That's it! The integration is ready.

Upload Endpoints

Avatar Upload

For user profile pictures:

// src/app/api/uploadthing/route.ts
export const ourFileRouter = {
  avatarUpload: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
    .middleware(async ({ req }) => {
      const session = await auth.api.getSession({ headers: req.headers });
      if (!session?.user) throw new Error("Unauthorized");
      return { userId: session.user.id };
    })
    .onUploadComplete(async ({ file, metadata }) => {
      console.log(`Avatar uploaded by ${metadata.userId}:`, file.url);

      // Update user profile with new avatar
      await db
        .update(user)
        .set({ image: file.url })
        .where(eq(user.id, metadata.userId));

      return { url: file.url };
    }),
};

Configuration:

  • Max size: 4MB
  • Max files: 1
  • Allowed: Images only (jpg, png, webp, gif)

Blog Image Upload

For blog post images:

blogImageUpload: f({ image: { maxFileSize: "8MB", maxFileCount: 10 } })
  .middleware(async ({ req }) => {
    const session = await auth.api.getSession({ headers: req.headers });
    if (!session?.user?.role !== "admin") {
      throw new Error("Unauthorized");
    }
    return { userId: session.user.id, folder: "blog" };
  })
  .onUploadComplete(async ({ file, metadata }) => {
    console.log(`Blog image uploaded:`, file.url);
    return { url: file.url };
  }),

Configuration:

  • Max size: 8MB
  • Max files: 10
  • Allowed: Images only
  • Admin only

Document Upload

For PDF and document uploads:

documentUpload: f({ pdf: { maxFileSize: "16MB", maxFileCount: 5 } })
  .middleware(async ({ req }) => {
    const session = await auth.api.getSession({ headers: req.headers });
    if (!session?.user) throw new Error("Unauthorized");
    return { userId: session.user.id };
  })
  .onUploadComplete(async ({ file, metadata }) => {
    // Log upload for audit
    await logSecurityEvent({
      eventType: "file_uploaded",
      userId: metadata.userId,
      metadata: { fileName: file.name, fileUrl: file.url },
    });

    return { url: file.url };
  }),

Configuration:

  • Max size: 16MB
  • Max files: 5
  • Allowed: PDF only

Client-Side Upload

React Component

"use client";
import { UploadButton, UploadDropzone } from "@/lib/uploadthing";

export function ImageUploader() {
  return (
    <UploadButton
      endpoint="avatarUpload"
      onClientUploadComplete={(res) => {
        console.log("Files uploaded:", res);
        toast.success("Upload complete!");
      }}
      onUploadError={(error: Error) => {
        toast.error(`Upload failed: ${error.message}`);
      }}
    />
  );
}

// Or use dropzone
export function ImageDropzone() {
  return (
    <UploadDropzone
      endpoint="blogImageUpload"
      onClientUploadComplete={(res) => {
        console.log("Uploaded:", res);
      }}
      onUploadError={(error) => {
        console.error("Error:", error);
      }}
    />
  );
}

Custom Upload UI

"use client";
import { useUploadThing } from "@/lib/uploadthing";
import { useState } from "react";

export function CustomUploader() {
  const [files, setFiles] = useState<File[]>([]);
  const { startUpload, isUploading } = useUploadThing("avatarUpload");

  const handleUpload = async () => {
    if (!files.length) return;

    const uploadedFiles = await startUpload(files);

    if (uploadedFiles) {
      console.log("Uploaded:", uploadedFiles);
      toast.success("Files uploaded successfully!");
    }
  };

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => {
          const fileList = e.target.files;
          if (fileList) {
            setFiles(Array.from(fileList));
          }
        }}
      />
      <Button onClick={handleUpload} disabled={isUploading}>
        {isUploading ? "Uploading..." : "Upload"}
      </Button>
    </div>
  );
}

Security

Authentication Required

All uploads require authentication:

.middleware(async ({ req }) => {
  const session = await auth.api.getSession({ headers: req.headers });

  if (!session?.user) {
    throw new Error("Unauthorized");
  }

  return { userId: session.user.id };
})

Role-Based Access

Restrict uploads by role:

.middleware(async ({ req }) => {
  const session = await auth.api.getSession({ headers: req.headers });

  if (session?.user?.role !== "admin") {
    throw new Error("Admin access required");
  }

  return { userId: session.user.id };
})

File Type Validation

Specified in endpoint configuration:

// Only images
f({ image: { ... } })

// Only PDFs
f({ pdf: { ... } })

// Only videos
f({ video: { ... } })

// Any file (use sparingly)
f({ blob: { ... } })

Size Limits

Per-endpoint size limits:

f({
  image: {
    maxFileSize: "4MB", // 4 megabytes
    maxFileCount: 5, // Max 5 files at once
  },
});

Audit Logging

Track all uploads:

.onUploadComplete(async ({ file, metadata }) => {
  await logSecurityEvent({
    eventType: "file_uploaded",
    userId: metadata.userId,
    severity: "low",
    description: `File uploaded: ${file.name}`,
    metadata: {
      fileName: file.name,
      fileSize: file.size,
      fileUrl: file.url,
      fileType: file.type,
    },
  });
})

Image Optimization

UploadThing automatically optimizes images:

  • Compression: Reduces file size
  • Format conversion: Converts to WebP when beneficial
  • Responsive images: Multiple sizes available

Accessing Optimized Images

// Original
const url = "https://utfs.io/f/abc123.jpg";

// Optimized (800px width)
const optimized = `${url}?w=800`;

// With quality
const quality = `${url}?w=800&q=80`;

// WebP format
const webp = `${url}?w=800&fmt=webp`;

Using in Next.js Image

import Image from "next/image";

<Image
  src={file.url}
  alt="Uploaded image"
  width={800}
  height={600}
  // UploadThing URLs work with Next.js Image
/>;

File Management

Listing User Files

// Track uploads in database
const userUploads = pgTable("user_uploads", {
  id: serial("id").primaryKey(),
  userId: text("user_id").references(() => user.id),
  fileName: text("file_name"),
  fileUrl: text("file_url"),
  fileSize: integer("file_size"),
  fileType: text("file_type"),
  uploadedAt: timestamp("uploaded_at").defaultNow(),
});

// Save on upload
.onUploadComplete(async ({ file, metadata }) => {
  await db.insert(userUploads).values({
    userId: metadata.userId,
    fileName: file.name,
    fileUrl: file.url,
    fileSize: file.size,
    fileType: file.type,
  });
})

Deleting Files

import { UTApi } from "uploadthing/server";

const utapi = new UTApi();

// Delete file
await utapi.deleteFiles(["file-key"]);

// Or multiple
await utapi.deleteFiles(["key1", "key2", "key3"]);

Advanced Usage

Progress Tracking

"use client";
import { useUploadThing } from "@/lib/uploadthing";

export function ProgressUploader() {
  const [progress, setProgress] = useState(0);

  const { startUpload } = useUploadThing("avatarUpload", {
    onUploadProgress: (p) => {
      setProgress(p);
    },
  });

  return (
    <div>
      {progress > 0 && <Progress value={progress} />}
      {/* Upload UI */}
    </div>
  );
}

Metadata

Pass custom metadata:

.middleware(async ({ req }) => {
  const session = await auth.api.getSession({ headers: req.headers });

  return {
    userId: session.user.id,
    uploadedAt: new Date().toISOString(),
    source: "blog-editor",
    // Custom metadata
  };
})

Multiple Endpoints

Create different endpoints for different use cases:

export const ourFileRouter = {
  avatarUpload: f({ image: { maxFileSize: "4MB" } })...,
  blogImageUpload: f({ image: { maxFileSize: "8MB" } })...,
  documentUpload: f({ pdf: { maxFileSize: "16MB" } })...,
  videoUpload: f({ video: { maxFileSize: "64MB" } })...,
}

Best Practices

  1. Always authenticate - Never allow anonymous uploads
  2. Validate file types - Restrict to needed types
  3. Set size limits - Prevent abuse and cost overruns
  4. Log uploads - Track who uploaded what
  5. Clean up unused files - Delete files when no longer needed
  6. Use appropriate limits - Don't allow 100MB uploads if 4MB suffices
  7. Handle errors gracefully - Show user-friendly error messages

Troubleshooting

Upload fails immediately

  • Check UPLOADTHING_TOKEN is set
  • Verify token is valid
  • Ensure user is authenticated

"Unauthorized" error

Check middleware authentication:

.middleware(async ({ req }) => {
  const session = await auth.api.getSession({ headers: req.headers });
  if (!session?.user) throw new Error("Unauthorized");
  return { userId: session.user.id };
})

File too large

  • Check client file size before upload
  • Increase maxFileSize if appropriate
  • Show user the size limit

Upload stuck at 100%

  • Check onUploadComplete doesn't have errors
  • Ensure database writes succeed
  • Check server logs

Pricing

UploadThing pricing (as of 2024):

  • Free Tier: 2GB storage + 2GB bandwidth
  • Pro: $10/month for 100GB storage + 100GB bandwidth
  • Pay as you go: Additional usage charged

Next Steps

On this page