Uploading Files

Uploading files is the first step in the process of uploading files to UploadThing. This page explains the general process of uploading files and how you can use the UploadThing API to upload files. There are two ways to upload files to UploadThing:

Client Side Uploads

The most common approach to uploading files is to use client-side uploads. With client side uploads, you do not have to pay ingress / egress fees for transferring the files binary data through your server. Your server instead will generate presigned URLs which the client will then use to upload the files to UploadThing.

Client Side Uploads Diagram

The easiest way to get started with client-side uploads it to define a File Router, expose it on your server using one of our adapters route handlers, and then upload the files using our built-in components or upload helpers. Here are some examples that should fit most of your needs, no matter what framework you're using:

FrameworkDocsExample
Backend AdaptersExpress, Fastify, Fetch, H3backend-adapters/server
Vanilla JavaScriptAPI Reference - Clientbackend-adapters/client-vanilla
ReactAPI Reference - Reactbackend-adapters/client-react
Vue.js-backend-adapters/client-vue
Next.js App RouterNext.js App Router Setupminimal-appdir
Next.js Pages RouterNext.js Page Router Setupminimal-pagedir
SolidStartSolidStart Setupminimal-solidstart
AstroGetting Started with Astrominimal-astro-react
SvelteKitGetting Started with SvelteKitminimal-sveltekit
NuxtGetting Started with Nuxtminimal-nuxt

If none of the above suits your needs, you can also build your own SDKs. In the following sections, we will describe how to do just that.

Building the backend adapter

The first step is to build a backend adapter. The adapter will be responsible for receiving the client request, validating that the request conforms to your requirements, and then retrieving and sending back the presigned URLs to the client.

We will not go in depth on the details here, however, you can refer to the implementations of the official TypeScript SDK or the community Python SDK for reference.

If you want your adapter to be compatible with the official frontend SDKs, it should follow this interface:

  • GET /api/uploadthing

  • POST /api/uploadthing

    • Input Query Parameters:
      • slug: string: The slug of the file route
      • actionType: "upload": The action type of the file route the client wish to perform
    • Input / Output JSON: Depends on action

Generating presigned URLs

Once your backend adapter has received and validated the request, you will next need to generate presigned URLs. First, generate some file keys for the files to be uploaded.

To generate a file key, generate a Sqids of your appId with { minLength: 12 }, then concatenate this with a file seed of your choice. The file seed can be anything you want, but it should be unique for each file, as well as url safe. In this example, we include a base64 encoding to ensure the file seed is url safe, but you can do this however you want.

Although we currently only offer a JavaScript SDK, here are some reference implementations you can use to generate valid file keys. We also plan on making this process easier in the future.

import * as Hash from "effect/Hash";
import SQIds, { defaultOptions } from "sqids";

// A simple function to shuffle the alphabet for the Sqids
function shuffle(str: string, seed: string) {
  const chars = str.split("");
  const seedNum = Hash.string(seed);

  let temp: string;
  let j: number;
  for (let i = 0; i < chars.length; i++) {
    j = ((seedNum % (i + 1)) + i) % chars.length;
    temp = chars[i];
    chars[i] = chars[j];
    chars[j] = temp;
  }

  return chars.join("");
}

function generateKey(appId: string, fileSeed: string) {
  // Hash and Encode the parts and apiKey as sqids
  const alphabet = shuffle(defaultOptions.alphabet, appId);

  const encodedAppId = new SQIds({ alphabet, minLength: 12 }).encode([
    Math.abs(Hash.string(appId)),
  ]);

  // We use a base64 encoding here to ensure the file seed is url safe, but
  // you can do this however you want
  const encodedFileSeed = encodeBase64(fileSeed);

  return `${encodedAppId}${encodedFileSeed}`;
}

The URL, to which you will upload the file, will depend on your app's region. You can find the list of regions in the regions documentation. The upload URL can then be constructed using the following format:

https://{{ REGION_ALIAS }}.ingest.uploadthing.com/{FILE_KEY}

Next, generate a signed URL by appending some query parameters to the upload URL, and finally a signature parameter containing the HMAC SHA256 digest of the URL:

const searchParams = new URLSearchParams({
  // Required
  expires: Date.now() + 60 * 60 * 1000, // 1 hour from now (you choose)
  "x-ut-identifier": "MY_APP_ID",
  "x-ut-file-name": "my-file.png",
  "x-ut-file-size": 131072,
  "x-ut-slug": "MY_FILE_ROUTE",

  // Optional
  "x-ut-file-type": "image/png",
  "x-ut-custom-id": "MY_CUSTOM_ID",
  "x-ut-content-disposition": "inline",
  "x-ut-acl": "public-read",
});

const url = new URL(
  `https://{{ REGION_ALIAS }}.ingest.uploadthing.com/${fileKey}`,
);
url.search = searchParams.toString();

const signature = hmacSha256(url, apiKey);
url.searchParams.append("signature", signature);

Return the signed URL(s) to the client.

Registering the upload

As you return the signed URL(s) to the client and the client starts uploading, you'll need to register the upload to UploadThing. This is so that the metadata, the result of running the file route middleware, can be retrieved later. Include also the callback URL and slug, this will be used by UploadThing to callback your server when the upload has been completed so that you can run the onUploadComplete / onUploadError callbacks for the upload.

curl -X POST https://{{ REGION_ALIAS }}.ingest.uploadthing.com/route-metadata \
    --header 'content-type: application/json' \
    --header 'x-uploadthing-api-key: YOUR_API_KEY' \
    --data '{
        "fileKeys": [
          "KEY_1",
        ],
        "metadata": {
          "uploadedBy": "user_123"
        },
        "callbackUrl": "https://your-domain.com/api/uploadthing",
        "callbackSlug": "imageUploader",
        "awaitServerData": false,
        "isDev": false
    }'

The two boolean flags are optional, and used to alter the behaviour of the upload.

  • awaitServerData: If set to true, the upload request will not respond immediately after file upload, but instead wait for your server to call the /callback-result endpoint with the result of running the onUploadComplete callback. Enable this only if your client needs to get data from the server callback as it will increase the duration of the upload.

  • isDev: If set to true, the response of this request will be a ReadableStream instead of JSON. The stream will be open for the duration of the upload and enqueue chunks as files are uploaded. This is in exchange of callback requests when your dev server cannot be reached from external servers. Each chunk will be a JSON string containing payload, signature and hook. Forward these to send a request to your dev server to simulate the callback request in development.

    curl -X POST http://localhost:3000/api/uploadthing \
      --header 'content-type: application/json' \
      --header 'x-uploadthing-signature: {{ SIGNATURE }}' \
      --header 'uploadthing-hook: {{ HOOK }}' \
      --data '{{ PAYLOAD }}'
    

Uploading the files

Uploading the files is as simple as submitting a PUT request to the signed URL.

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

await fetch(presigned.url, {
  method: "PUT",
  body: formData,
});

If you want to implement resumable uploads, you can additionally include the Range header in the request, with the starting byte offset. To get the range start, you can send a HEAD request to the presigned URL and get the x-ut-range-start header.

const rangeStart = await fetch(presigned.url, { method: "HEAD" }).then((res) =>
  parseInt(res.headers.get("x-ut-range-start") ?? "0", 10),
);
await fetch(presigned.url, {
  method: "PUT",
  headers: {
    Range: `bytes=${rangeStart}-`,
  },
  body: file.slice(rangeStart),
});

Handling the callback request

When your file has been uploaded, the UploadThing API will send a callback request (similar to a webhook) to the callbackUrl you provided when requesting the presigned URLs. The callback request will contain the file information along with the metadata.

You can identify the hook by the presence of the uploadthing-hook header and verify it using the x-uploadthing-signature header. The signature is a HMAC SHA256 of the request body signed using your API key, which you can verify to ensure the request is authentic and originates from the UploadThing server.

After you have verified the request is authentic, run the onUploadComplete function for the file route. The data returned from the callback can then be submitted back to the UploadThing API using the /callback-result endpoint. Once the data has been submitted, the client upload request will finish assuming the awaitServerData flag was set to true when registerring the upload in a previous step.

Congratulations, you have now uploaded your files to UploadThing.

Server Side Uploads

Sometimes you may either produce the file on your server, or want to validate the file's content before uploading it. In these cases, you can use server-side uploads and first submit the files to your server and then from your server upload the files to UploadThing.

Server side uploads

Going forward, we will assume you have already received the file on your server, for example using FormData from a file upload form.

Fortunately, server side uploads are very straight forward, and in many ways similar to client side uploads. Generating presigned URLs is the same for server-side uploads as for client-side uploads. The only difference is that you do not have to include the x-ut-slug search parameter in the URL.

After you have generated the presigned URLs, you can upload the files to UploadThing using the same steps as explained for client-side uploads. The response of the request will contain the file information for successful uploads, or an error message if the upload failed.

And that's it. There is no need to register the upload to UploadThing, or handle any callback requests when doing server-side uploads.

Was this page helpful?