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_TOKENThe token must contain:
userId- User IDcompanyId- 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:
{
"filename": "logo.png",
"contentType": "image/png",
"fileSize": 50000
}Response (200 OK):
{
"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 filename401 Unauthorized- Missing or invalid JWT token500 Internal Server Error- Failed to generate presigned URL
Client Usage:
// 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:
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):
{
"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 file401 Unauthorized- Missing or invalid JWT token500 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 returnoffset(optional, default: 0) - Pagination offset
Example:
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):
{
"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 token500 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:
const response = await fetch(`/api/upload/images/${imageId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const { success, message } = await response.json();Response (200 OK):
{
"success": true,
"message": "Image deleted successfully"
}Error Responses:
401 Unauthorized- Missing or invalid JWT token404 Not Found- Image not found or access denied500 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/jpgimage/pngimage/gifimage/webpimage/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.pngCDN 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:
{
"error": "Error Type",
"message": "Human-readable error message",
"statusCode": 400
}Common Error Codes:
400- Validation error (invalid file type, size, etc.)401- Authentication required404- Resource not found429- Rate limit exceeded500- Server error
Best Practices ​
For Client-Side Uploads (Presigned URLs) ​
- Always validate files on the client before requesting presigned URL
- Show upload progress to improve UX
- Handle errors gracefully (network failures, timeouts)
- Use presigned URLs immediately (they expire after 1 hour)
For Direct Uploads ​
- Use for smaller files (< 1MB recommended)
- Show upload progress using
XMLHttpRequestor similar - Compress images on the client before upload if possible
General ​
- Store
imageIdin your database to track which images are used where - Delete unused images to save storage costs
- Use CDN URLs for displaying images (better performance)
- Implement retry logic for failed uploads
Examples ​
React Component (Presigned URL) ​
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 ​
- CDN Setup - Implementation details