/ astro-integrations / How to Integrate Uploadthing with Astro: Complete Guide
astro-integrations 6 min read

How to Integrate Uploadthing with Astro: Complete Guide

Step-by-step guide to integrating Uploadthing with your Astro website.

Uploadthing is a file upload service built specifically for TypeScript developers. Instead of cobbling together S3 buckets, presigned URLs, and upload validation yourself, Uploadthing gives you a type-safe API that handles file uploads from end to end. You define what files are allowed (type, size, count), and Uploadthing handles the rest: client-side uploads, server-side validation, and storage.

For Astro projects that need file uploads (user avatars, blog images, document attachments), Uploadthing eliminates a lot of boilerplate. The files are stored on Uploadthing's infrastructure and served through a CDN.

Prerequisites

  • Node.js 18+
  • An Astro project with SSR enabled (npm create astro@latest)
  • An Uploadthing account (free tier includes 2GB storage and 2GB bandwidth/month)

Installation

npm install uploadthing @uploadthing/react

If you are not using React islands in Astro, you can skip @uploadthing/react and use the core library directly.

Configuration

Get your API keys from the Uploadthing dashboard and add them to .env:

UPLOADTHING_SECRET=sk_live_xxxx
UPLOADTHING_APP_ID=your_app_id

Create the upload router that defines what files your app accepts:

// src/lib/uploadthing.ts
import { createUploadthing, type FileRouter } from "uploadthing/server";

const f = createUploadthing();

export const uploadRouter = {
  blogImage: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
    .middleware(async ({ req }) => {
      // Add auth checks here if needed
      return { uploadedBy: "admin" };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      console.log("Upload complete:", file.url);
      return { url: file.url };
    }),

  avatar: f({ image: { maxFileSize: "2MB", maxFileCount: 1 } })
    .middleware(async ({ req }) => {
      return { uploadedBy: "user" };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      return { url: file.url };
    }),

  document: f({
    pdf: { maxFileSize: "16MB", maxFileCount: 5 },
    "image/png": { maxFileSize: "4MB", maxFileCount: 5 },
  })
    .middleware(async () => {
      return {};
    })
    .onUploadComplete(async ({ file }) => {
      return { url: file.url };
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof uploadRouter;

API Route Setup

Create an API route that Uploadthing uses for its upload handshake:

// src/pages/api/uploadthing.ts
import type { APIRoute } from "astro";
import { createRouteHandler } from "uploadthing/server";
import { uploadRouter } from "../../lib/uploadthing";

const handlers = createRouteHandler({
  router: uploadRouter,
});

export const GET: APIRoute = async (context) => {
  return handlers.GET(context.request);
};

export const POST: APIRoute = async (context) => {
  return handlers.POST(context.request);
};

Client-Side Upload with Vanilla JavaScript

For a framework-agnostic approach that works with any Astro page:

---
// src/components/FileUploader.astro
---

<div id="upload-zone">
  <input type="file" id="file-input" accept="image/*" />
  <button id="upload-btn" disabled>Upload</button>
  <p id="upload-status"></p>
  <img id="upload-preview" style="max-width: 300px; display: none;" alt="Uploaded file preview" />
</div>

<script>
  const input = document.getElementById("file-input") as HTMLInputElement;
  const btn = document.getElementById("upload-btn") as HTMLButtonElement;
  const status = document.getElementById("upload-status") as HTMLParagraphElement;
  const preview = document.getElementById("upload-preview") as HTMLImageElement;

  input.addEventListener("change", () => {
    btn.disabled = !input.files?.length;
  });

  btn.addEventListener("click", async () => {
    const file = input.files?.[0];
    if (!file) return;

    status.textContent = "Uploading...";
    btn.disabled = true;

    const formData = new FormData();
    formData.append("file", file);

    try {
      const res = await fetch("/api/upload", {
        method: "POST",
        body: formData,
      });
      const data = await res.json();

      if (data.url) {
        status.textContent = "Upload complete!";
        preview.src = data.url;
        preview.style.display = "block";
      } else {
        status.textContent = "Error: " + (data.error || "Unknown error");
      }
    } catch (err) {
      status.textContent = "Upload failed";
    }

    btn.disabled = false;
  });
</script>

React Upload Component (Astro Island)

If you use React islands in Astro, Uploadthing provides ready-made components:

// src/components/UploadButton.tsx
import { UploadButton } from "@uploadthing/react";
import type { OurFileRouter } from "../lib/uploadthing";

export default function ImageUploader() {
  return (
    <UploadButton<OurFileRouter, "blogImage">
      endpoint="blogImage"
      onClientUploadComplete={(res) => {
        console.log("Files:", res);
        alert("Upload complete! URL: " + res[0]?.url);
      }}
      onUploadError={(error: Error) => {
        alert("Error: " + error.message);
      }}
    />
  );
}

Use it in an Astro page:

---
import ImageUploader from "../components/UploadButton";
---

<ImageUploader client:load />

Handling Upload Callbacks

Uploadthing calls your onUploadComplete callback after a successful upload. Use this to save the file URL to your database or update your content:

blogImage: f({ image: { maxFileSize: "4MB" } })
  .middleware(async ({ req }) => {
    // Validate the user session
    const session = await getSession(req);
    if (!session) throw new Error("Unauthorized");
    return { userId: session.userId };
  })
  .onUploadComplete(async ({ metadata, file }) => {
    // Save to your database
    await db.images.create({
      url: file.url,
      key: file.key,
      userId: metadata.userId,
      size: file.size,
    });
    return { url: file.url };
  }),

Deleting Files

Remove files from Uploadthing when they are no longer needed:

// src/lib/uploadthing.ts (add to existing)
import { UTApi } from "uploadthing/server";

export const utapi = new UTApi();

export async function deleteFile(fileKey: string): Promise<void> {
  await utapi.deleteFiles(fileKey);
}

Use it in an API route:

// src/pages/api/delete-file.ts
import type { APIRoute } from "astro";
import { deleteFile } from "../../lib/uploadthing";

export const DELETE: APIRoute = async ({ request }) => {
  const { fileKey } = await request.json();
  await deleteFile(fileKey);
  return new Response(JSON.stringify({ success: true }), { status: 200 });
};

Production Tips

  1. Always validate in middleware. The middleware function runs server-side before any upload begins. Use it to check authentication, rate limits, and user permissions.

  2. Store file keys, not just URLs. Uploadthing file keys are needed to delete or manage files later. Save both the URL (for display) and the key (for management) in your database.

  3. Set strict file limits. Be specific about file types and sizes for each endpoint. A blog image uploader should not accept 100MB videos.

  4. Handle errors gracefully. The onUploadError callback on the client receives specific error types (file too large, wrong type, server error). Show helpful messages to the user.

  5. Use webhooks for production. Instead of relying solely on onUploadComplete, configure Uploadthing webhooks to receive upload notifications. This ensures you do not miss completions if the user closes their browser mid-upload.

Alternatives to Consider

  • Cloudflare R2 + custom upload logic if you want full control over storage and zero egress fees.
  • Supabase Storage if you are already using Supabase and want uploads integrated with your auth and database.
  • tus protocol libraries if you need resumable uploads for very large files.

Wrapping Up

Uploadthing takes the complexity out of file uploads for TypeScript projects. The type-safe file router, built-in validation, and managed storage mean you spend less time on upload infrastructure and more time on your actual product. For Astro SSR apps that need file uploads, it is one of the fastest paths from zero to working uploads. Define your routes, drop in a component, and files land safely in the cloud.