DEV Community

Cover image for File uploads for a social media app with Tigris
Twilight
Twilight

Posted on

File uploads for a social media app with Tigris

Social media apps live and die on media handling. Users post photos, upload videos, share documents. Your backend could route every file through your servers, but that burns bandwidth and adds latency. Tigris gives you a way to push files straight from the browser to object storage, with the data replicated across multiple regions worldwide.

Tigris is an S3-compatible object storage service. Your data lives in multiple regions at once. A user in Tokyo uploads a photo; a follower in Berlin loads it from a nearby cache. You do not pick a primary region. Tigris handles distribution.

This post walks through using the Tigris JavaScript SDK to handle file uploads for a social media app.

Setup

Install the SDK:

npm install @tigrisdata/storage
Enter fullscreen mode Exit fullscreen mode

Create a bucket at console.storage.dev and grab an access key. Put the credentials in your environment:

TIGRIS_STORAGE_ACCESS_KEY_ID=tid_your_access_key
TIGRIS_STORAGE_SECRET_ACCESS_KEY=tsec_your_secret_key
TIGRIS_STORAGE_BUCKET=social-media-uploads
Enter fullscreen mode Exit fullscreen mode

Server-side uploads

Some uploads should go through your server. Profile pictures, for instance, where you want to validate image dimensions before storing. The put function handles this.

import { put } from "@tigrisdata/storage";

async function uploadAvatar(userId: string, imageBuffer: Buffer) {
  const result = await put(`avatars/${userId}.jpg`, imageBuffer, {
    contentType: "image/jpeg",
    access: "public",
    allowOverwrite: true,
  });

  if (result.error) {
    throw result.error;
  }

  return result.data?.url;
}
Enter fullscreen mode Exit fullscreen mode

The returned url points to the uploaded file. Because access is "public", the URL works without authentication. Any user on the platform can load the avatar.

The SDK infers content type from the file extension when you omit contentType. Setting it explicitly is safer when the extension might not match the actual format.

Large files with multipart upload

Videos are the bulk of social media storage. A 500 MB video clip does not fit in a single request. Pass multipart: true and the SDK splits the file into chunks and uploads them in parallel.

import { put } from "@tigrisdata/storage";

async function uploadVideo(file: ReadableStream) {
  const result = await put("videos/new-post.mp4", file, {
    multipart: true,
    contentType: "video/mp4",
    access: "private",
    onUploadProgress: ({ loaded, total, percentage }) => {
      console.log(`${percentage}% uploaded`);
    },
  });

  if (result.error) {
    throw result.error;
  }

  return result.data;
}
Enter fullscreen mode Exit fullscreen mode

The onUploadProgress callback fires as chunks complete. Wire it to a progress bar in the UI and users can see how far along the upload is.

Private files need a signed URL for access. Generate one with getPresignedUrl:

import { getPresignedUrl } from "@tigrisdata/storage";

const url = await getPresignedUrl("videos/new-post.mp4", {
  operation: "get",
  expiresIn: 3600,
});
Enter fullscreen mode Exit fullscreen mode

That URL works for one hour. After that, generate a new one.

Client-side uploads

Routing files through your server wastes bandwidth. The Tigris SDK ships a browser-compatible module that uploads files directly from the client, skipping your server entirely.

Your server generates a presigned URL, the browser uploads to Tigris using that URL, and your server never touches the file bytes. The @tigrisdata/storage/client package wraps this into a single upload function.

First, add an API route that the client module calls to initiate the upload:

// pages/api/upload.ts (Next.js example)
import { getPresignedUrl } from "@tigrisdata/storage";

export default async function handler(req, res) {
  const url = await getPresignedUrl(`uploads/${req.body.filename}`, {
    operation: "put",
    contentType: req.body.contentType,
    expiresIn: 3600,
  });

  if (url.error) {
    return res.status(500).json({ error: url.error });
  }

  res.status(200).json(url.data);
}
Enter fullscreen mode Exit fullscreen mode

Then in the browser:

<input type="file" onchange="handleUpload(event)" />

<script type="module">
import { upload } from "@tigrisdata/storage/client";

async function handleUpload(event) {
  const file = event.target.files[0];

  const result = await upload(`posts/${file.name}`, file, {
    url: "/api/upload",
    multipart: true,
    contentType: file.type,
    onUploadProgress: ({ percentage }) => {
      updateProgressBar(percentage);
    },
  });

  if (result.error) {
    console.error(result.error);
    return;
  }

  console.log("Uploaded:", result.data.url);
}
</script>
Enter fullscreen mode Exit fullscreen mode

The url option tells the client module where your server endpoint is. The client calls that endpoint to get a presigned URL, then uploads the file bytes straight to Tigris. Your server CPU and bandwidth stay free.

Listing and deleting files

A user deletes a post. You need to remove the associated media. The SDK provides list and remove for this.

import { list, remove } from "@tigrisdata/storage";

async function deletePostMedia(postId: string) {
  const files = await list({ prefix: `posts/${postId}/` });

  if (files.error) {
    throw files.error;
  }

  for (const item of files.data?.items ?? []) {
    await remove(item.key);
  }
}
Enter fullscreen mode Exit fullscreen mode

list with a prefix filter returns only objects under that path. Paginate with paginationToken when a prefix contains more than 100 objects.

Error handling

Every SDK function returns { data, error }. Check error before using data. This pattern works well in both server and client code:

const result = await put("photo.jpg", buffer);

if (result.error) {
  console.error("Upload failed:", result.error);
  return;
}

console.log("Stored at:", result.data?.url);
Enter fullscreen mode Exit fullscreen mode

No exceptions to catch. No guessing about what went wrong. The error object contains enough information to surface a message to the user or log for debugging.

Why Tigris

Most object storage providers charge for egress. Social media apps serve the same files over and over; followers reload images, videos get replayed. Egress fees add up fast. Tigris charges zero for egress.

The global distribution is the other reason. You deploy one bucket. Tigris replicates the data across regions. Users in different continents load files from nearby infrastructure without any extra configuration from you.

The SDK is simpler than configuring the AWS S3 SDK. Three environment variables and you are done. No endpoint URLs to manage, no region parameters to pass on every call.

The Tigris JavaScript SDK handles the common file operations a social media app needs: upload, download, list, delete, and presigned URLs. Client-side uploads keep your server costs down. Global distribution keeps load times low for users everywhere.

Top comments (0)