Skip to content

Overview ​

The File Upload API provides endpoints for uploading, managing, and deleting images in TendSocial. All endpoints require JWT authentication and enforce multi-tenant isolation.

Base URL: /api/upload


Authentication ​

All endpoints require a valid JWT token in the Authorization header:

Authorization: Bearer YOUR_JWT_TOKEN

The token must contain:

  • userId - User ID
  • companyId - Company ID (for tenant isolation)

Endpoints ​

1. Generate Presigned URL ​

Generate a presigned URL for client-side upload (recommended for large files).

Endpoint: POST /api/upload/presigned-url

Request Body:

json
{
  "filename": "logo.png",
  "contentType": "image/png",
  "fileSize": 50000
}

Response (200 OK):

json
{
  "uploadUrl": "https://...r2.cloudflarestorage.com/...",
  "key": "company-123/images/user-456/1732713600000-abc123-logo.png",
  "cdnUrl": "https://cdn.tendsocial.com/company-123/images/user-456/...",
  "imageId": "img-uuid-123"
}

Error Responses:

  • 400 Bad Request - Invalid file type, size, or filename
  • 401 Unauthorized - Missing or invalid JWT token
  • 500 Internal Server Error - Failed to generate presigned URL

Client Usage:

javascript
// 1. Get presigned URL
const response = await fetch('/api/upload/presigned-url', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    filename: file.name,
    contentType: file.type,
    fileSize: file.size
  })
});

const { uploadUrl, cdnUrl, imageId } = await response.json();

// 2. Upload file directly to R2
await fetch(uploadUrl, {
  method: 'PUT',
  headers: {
    'Content-Type': file.type
  },
  body: file
});

// 3. Use cdnUrl to display the image
console.log('Image uploaded:', cdnUrl);

2. Direct Upload ​

Upload a file directly through the API server (for smaller files).

Endpoint: POST /api/upload/direct

Request: Multipart form data with file field

Example:

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

const response = await fetch('/api/upload/direct', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`
  },
  body: formData
});

const { cdnUrl, imageId } = await response.json();

Response (200 OK):

json
{
  "imageId": "img-uuid-123",
  "cdnUrl": "https://cdn.tendsocial.com/...",
  "key": "company-123/images/user-456/...",
  "filename": "logo.png"
}

Error Responses:

  • 400 Bad Request - No file uploaded or invalid file
  • 401 Unauthorized - Missing or invalid JWT token
  • 500 Internal Server Error - Upload failed

3. List Images ​

Retrieve a paginated list of uploaded images for the current user.

Endpoint: GET /api/upload/images

Query Parameters:

  • limit (optional, default: 50, max: 100) - Number of images to return
  • offset (optional, default: 0) - Pagination offset

Example:

javascript
const response = await fetch('/api/upload/images?limit=20&offset=0', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const { images, total, limit, offset } = await response.json();

Response (200 OK):

json
{
  "images": [
    {
      "id": "img-uuid-123",
      "filename": "logo.png",
      "cdnUrl": "https://cdn.tendsocial.com/...",
      "format": "png",
      "width": 1200,
      "height": 630,
      "createdAt": "2024-11-27T12:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Error Responses:

  • 401 Unauthorized - Missing or invalid JWT token
  • 500 Internal Server Error - Failed to retrieve images

4. Delete Image ​

Delete an uploaded image (removes from both R2 storage and database).

Endpoint: DELETE /api/upload/images/:id

Path Parameters:

  • id - Image ID (UUID)

Example:

javascript
const response = await fetch(`/api/upload/images/${imageId}`, {
  method: 'DELETE',
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const { success, message } = await response.json();

Response (200 OK):

json
{
  "success": true,
  "message": "Image deleted successfully"
}

Error Responses:

  • 401 Unauthorized - Missing or invalid JWT token
  • 404 Not Found - Image not found or access denied
  • 500 Internal Server Error - Failed to delete image

Security: Users can only delete images they uploaded within their company.


File Validation ​

Allowed File Types ​

  • image/jpeg, image/jpg
  • image/png
  • image/gif
  • image/webp
  • image/svg+xml

File Size Limits ​

  • Maximum: 10MB per file
  • Minimum: Greater than 0 bytes

Filename Restrictions ​

  • Cannot contain .., /, or \
  • Maximum length: 255 characters

File Organization ​

Files are organized with multi-tenant isolation:

{bucket}/
├── {companyId-1}/
│   └── images/
│       ├── {userId-1}/
│       │   ├── {timestamp}-{uuid}-{filename}.{ext}
│       │   └── ...
│       └── {userId-2}/
│           └── ...
└── {companyId-2}/
    └── ...

Example Key:

company-abc123/images/user-def456/1732713600000-abc123-uuid-logo.png

CDN URLs ​

If CDN_DOMAIN environment variable is set, files are served via custom domain:

https://cdn.tendsocial.com/{key}

Otherwise, direct R2 URLs are used:

https://{bucket}.s3.{region}.amazonaws.com/{key}

Benefits of CDN:

  • ✅ Free egress (no bandwidth charges)
  • ✅ Global caching for faster delivery
  • ✅ Custom branding
  • ✅ DDoS protection

Rate Limiting ​

Upload endpoints are subject to the global rate limit:

  • 100 requests per minute per IP address

Exceeding this limit returns 429 Too Many Requests.


Error Handling ​

All error responses follow this format:

json
{
  "error": "Error Type",
  "message": "Human-readable error message",
  "statusCode": 400
}

Common Error Codes:

  • 400 - Validation error (invalid file type, size, etc.)
  • 401 - Authentication required
  • 404 - Resource not found
  • 429 - Rate limit exceeded
  • 500 - Server error

Best Practices ​

For Client-Side Uploads (Presigned URLs) ​

  1. Always validate files on the client before requesting presigned URL
  2. Show upload progress to improve UX
  3. Handle errors gracefully (network failures, timeouts)
  4. Use presigned URLs immediately (they expire after 1 hour)

For Direct Uploads ​

  1. Use for smaller files (< 1MB recommended)
  2. Show upload progress using XMLHttpRequest or similar
  3. Compress images on the client before upload if possible

General ​

  1. Store imageId in your database to track which images are used where
  2. Delete unused images to save storage costs
  3. Use CDN URLs for displaying images (better performance)
  4. Implement retry logic for failed uploads

Examples ​

React Component (Presigned URL) ​

tsx
import { useState } from 'react';

function ImageUpload() {
  const [uploading, setUploading] = useState(false);
  const [imageUrl, setImageUrl] = useState<string | null>(null);

  const handleUpload = async (file: File) => {
    setUploading(true);

    try {
      // 1. Get presigned URL
      const presignedResponse = await fetch('/api/upload/presigned-url', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${getToken()}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          filename: file.name,
          contentType: file.type,
          fileSize: file.size
        })
      });

      const { uploadUrl, cdnUrl } = await presignedResponse.json();

      // 2. Upload to R2
      await fetch(uploadUrl, {
        method: 'PUT',
        headers: { 'Content-Type': file.type },
        body: file
      });

      // 3. Display uploaded image
      setImageUrl(cdnUrl);
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
      {imageUrl && <img src={imageUrl} alt="Uploaded" />}
    </div>
  );
}

See Also ​

TendSocial Documentation