Overview

TurboStack provides a unified media service with a provider pattern that allows you to easily switch between different media providers like UploadThing and Cloudinary.
Important: When adding or modifying media endpoints, always update the OpenAPI/Swagger documentation! This ensures the API documentation stays in sync with your code.

Provider Architecture

The media service uses an abstraction layer that allows you to swap providers without changing your application code:
┌──────────────────────────────────────┐
│      MediaService (Facade)           │
├──────────────────────────────────────┤
│      MediaProvider Interface         │
├──────────────────────────────────────┤
│  UploadThingProvider │ CloudinaryProvider │
└──────────────────────────────────────┘

API Endpoints

POST /api/media/upload

Upload a file using the active provider

GET /api/media/:id

Get media file details

DELETE /api/media/:id

Delete a media file

POST /api/media/transform

Apply transformations to media

Configuration

1

Environment Variables

Add provider credentials to .env:
# Media Provider (uploadthing | cloudinary)
MEDIA_PROVIDER=uploadthing

# UploadThing
UPLOADTHING_TOKEN=your_token_here

# Cloudinary
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
2

Provider Setup

Configure the media service in lib/media/media.service.ts:
import { MediaService } from "@/lib/media/media.service";
import { UploadThingProvider } from "@/lib/media/providers/uploadthing";
import { CloudinaryProvider } from "@/lib/media/providers/cloudinary";

// Select provider based on environment
const provider = process.env.MEDIA_PROVIDER === "cloudinary"
  ? new CloudinaryProvider()
  : new UploadThingProvider();

export const mediaService = new MediaService(provider);

Usage Examples

Upload a File

// POST /api/media/upload
const response = await fetch("http://localhost:4101/api/media/upload", {
  method: "POST",
  body: formData,
});

const result = await response.json();
// {
//   success: true,
//   data: {
//     id: "file_123",
//     url: "https://...",
//     name: "image.jpg",
//     size: 245632,
//     type: "image/jpeg"
//   }
// }

Apply Transformations

// POST /api/media/transform
const response = await fetch("http://localhost:4101/api/media/transform", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    fileId: "file_123",
    transformations: {
      width: 800,
      height: 600,
      format: "webp",
      quality: 80,
    },
  }),
});

Delete a File

// DELETE /api/media/:id
await fetch("http://localhost:4101/api/media/file_123", {
  method: "DELETE",
});

Frontend Integration

Using UploadThing (React)

import { UploadButton } from "@uploadthing/react";
import type { OurFileRouter } from "../api/lib/uploadthing";

export function ImageUploader() {
  return (
    <UploadButton<OurFileRouter, "imageUploader">
      endpoint="imageUploader"
      onClientUploadComplete={(res) => {
        console.log("Upload completed:", res);
      }}
      onUploadError={(error) => {
        console.error("Upload error:", error);
      }}
    />
  );
}

Custom Upload Component

export function MediaUploader() {
  const handleUpload = async (file: File) => {
    const formData = new FormData();
    formData.append("file", file);

    const response = await fetch("/api/media/upload", {
      method: "POST",
      body: formData,
    });

    const result = await response.json();
    return result.data;
  };

  return (
    <input
      type="file"
      onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) handleUpload(file);
      }}
    />
  );
}

OpenAPI Documentation

Remember: Every media endpoint change MUST be documented in Swagger!

Example: Documented Upload Endpoint

// src/routes/upload.ts
export const mediaRoutes = new Elysia({ prefix: "/media" }).post(
  "/upload",
  async ({ body }) => {
    const result = await mediaService.upload(body.file);
    return { success: true, data: result };
  },
  {
    body: t.Object({
      file: t.File(),
    }),
    detail: {
      tags: ["Media"],
      summary: "Upload a file",
      description: "Upload a file using the active media provider",
      requestBody: {
        content: {
          "multipart/form-data": {
            schema: {
              type: "object",
              properties: {
                file: {
                  type: "string",
                  format: "binary",
                },
              },
            },
          },
        },
      },
      responses: {
        200: {
          description: "File uploaded successfully",
          content: {
            "application/json": {
              example: {
                success: true,
                data: {
                  id: "file_123",
                  url: "https://example.com/file.jpg",
                  name: "file.jpg",
                  size: 245632,
                  type: "image/jpeg",
                },
              },
            },
          },
        },
        400: {
          description: "Invalid file or upload failed",
        },
      },
    },
  },
);

Providers

UploadThing

  • Type-safe file upload solution
  • Built-in React components
  • Automatic CDN delivery
  • Max file size: 4MB (configurable)

Cloudinary

  • Advanced media transformations
  • Image/video optimization
  • Responsive images
  • AI-powered features

Switching Providers

Simply change the MEDIA_PROVIDER environment variable:
# Use UploadThing
MEDIA_PROVIDER=uploadthing

# Use Cloudinary
MEDIA_PROVIDER=cloudinary
No code changes required! The MediaService facade handles the switching automatically.

Best Practices

When you create or modify a media endpoint, add comprehensive OpenAPI documentation with: - Tags (use “Media”) - Summary and description - Request/response examples - Error responses (400, 404, 500)
Always validate file types and sizes on the backend before uploading.
Apply transformations to optimize images for web delivery (resize, compress, format conversion).
Provide clear error messages when uploads fail or files are not found.