Professional media hosting backend for blogging platforms - Built entirely on Cloudflare's free tier with production-ready architecture, comprehensive logging, and automated CI/CD.
⚡ Can't Delete Files? → Read This
📁 Files Not Showing in Gallery? → Fixed!
- Setup Guide - Get started in 5 minutes
- API Usage - Integration examples for your project
- Troubleshooting - Common issues and solutions
Admin UI: https://cdn.aadityahasabnis.workers.dev/
API Endpoint: https://cdn.aadityahasabnis.workers.dev/api
- File Upload - Images, videos, and documents via authenticated REST API
- R2 Storage - Zero egress fees, S3-compatible object storage
- D1 Database - SQLite at the edge for metadata and search
- KV Cache - Sub-millisecond global reads for gallery queries
- CDN Delivery - Cloudflare's global network with automatic optimization
- Image Transforms - Built-in resize, crop, format conversion
- Custom Error Classes - Structured error hierarchy with proper status codes
- Structured Logging - JSON logs with request ID correlation
- Request Tracing - Track requests across all service layers
- Environment Validation - Fail fast on startup with missing config
- Type Safety - Full TypeScript with strict mode
- Zero Magic Numbers - All constants centralized and documented
- Comprehensive JSDoc - Every function fully documented
- Modern Test UI - Professional admin panel with drag-and-drop upload
- CI/CD Pipeline - Automatic deployment via GitHub Actions
- Complete Documentation - Commands, environment variables, API reference
- Rate Limiting - Built-in protection against abuse
- Cache Invalidation - Smart cache busting on mutations
- Node.js 18+
- Cloudflare account (free tier works)
- Wrangler CLI
# Clone the repository
git clone <your-repo-url>
cd media-service
# Install dependencies
npm install
# Authenticate with Cloudflare
npx wrangler login# Create Cloudflare resources
npx wrangler r2 bucket create media-storage
npx wrangler d1 create media-db
npx wrangler kv namespace create MEDIA_CACHE
# Initialize database
npx wrangler d1 execute media-db --file=schema.sql
# Set production secret
npx wrangler secret put ADMIN_API_KEY- Update
wrangler.jsoncwith your resource IDs (from create commands above) - Create
.dev.varsfor local development:ADMIN_API_KEY=your-admin-api-key - Configure CDN_BASE_URL in
wrangler.jsonc(see ENV.md)
npm run devVisit http://localhost:8787/ for the admin panel.
npm run deploymedia-service/
├── src/
│ ├── constants/ # Centralized constants (no magic numbers)
│ │ └── index.ts
│ ├── config/ # Environment validation & configuration
│ │ └── index.ts
│ ├── lib/ # Core infrastructure
│ │ ├── errors.ts # Custom error class hierarchy
│ │ └── logger.ts # Structured JSON logger
│ ├── middleware/ # Request processing
│ │ ├── auth.ts # API key authentication & rate limiting
│ │ └── requestId.ts # Request ID generation & logging
│ ├── services/ # Business logic layer
│ │ ├── r2.ts # R2 object storage operations
│ │ ├── database.ts # D1 database queries
│ │ └── cache.ts # KV cache management
│ ├── endpoints/ # API route handlers
│ │ ├── upload.ts # POST /api/media/upload
│ │ ├── listMedia.ts # GET /api/media/list
│ │ └── deleteMedia.ts # DELETE /api/media/delete
│ ├── utils/ # Helper functions
│ │ ├── fileName.ts # Collision-safe file naming
│ │ ├── validation.ts # Input validation
│ │ └── response.ts # Response formatting
│ ├── ui/ # Admin interface
│ │ └── testUI.ts # Professional gallery UI
│ ├── types.ts # TypeScript type definitions
│ └── index.ts # Main entry point & routes
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD automation
├── schema.sql # D1 database schema
├── wrangler.jsonc # Cloudflare configuration
├── .dev.vars # Local secrets (gitignored)
├── COMMANDS.md # Complete command reference
├── ENV.md # Environment variable documentation
└── README.md # This file
POST /api/media/uploadHeaders:
x-api-key: YOUR_ADMIN_KEY(required)Content-Type: multipart/form-data
Body (form-data):
| Field | Type | Required | Description |
|---|---|---|---|
file |
File | Yes | The file to upload |
folder |
string | No | Folder name (default: "root") |
type |
string | No | "image" | "video" | "file" (auto-detected) |
tags |
string | No | Comma-separated tags |
Response:
{
"success": true,
"file_key": "images/blog/1741300000000-x7k3p-cat.png",
"public_url": "https://cdn.yourdomain.com/images/blog/1741300000000-x7k3p-cat.png",
"mime_type": "image/png",
"size": 120394
}GET /api/media/listQuery Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
type |
string | (all) | Filter: "image" | "video" | "file" |
folder |
string | (all) | Filter by folder name |
limit |
number | 50 | Items per page (1-100) |
page |
number | 1 | Page number (1+) |
Response:
{
"success": true,
"files": [
{
"id": 1,
"file_key": "images/blog/cat.png",
"public_url": "https://cdn.yourdomain.com/images/blog/cat.png",
"file_type": "image",
"mime_type": "image/png",
"size": 120394,
"folder": "blog",
"tags": "tutorial,cloudflare",
"created_at": "2026-03-07T12:00:00.000Z"
}
],
"total": 247,
"page": 1,
"limit": 50,
"pages": 5,
"from_cache": true
}DELETE /api/media/deleteHeaders:
x-api-key: YOUR_ADMIN_KEY(required)Content-Type: application/json
Body:
{
"file_key": "images/blog/1741300000000-x7k3p-cat.png"
}Response:
{
"success": true,
"file_key": "images/blog/1741300000000-x7k3p-cat.png",
"message": "File deleted successfully"
}Alternative: Delete by ID
DELETE /api/media/:idGET /api/media/statsResponse:
{
"success": true,
"stats": {
"total_files": 247,
"total_size": 1073741824,
"by_type": {
"image": 150,
"video": 50,
"file": 47
},
"by_folder": {
"blog": 120,
"gallery": 80,
"documents": 47
},
"recent_uploads": 15
}
}GET /api/media/foldersResponse:
{
"success": true,
"folders": [
{ "folder": "blog", "count": 120 },
{ "folder": "gallery", "count": 80 },
{ "folder": "root", "count": 47 }
]
}GET /healthResponse:
{
"success": true,
"status": "healthy",
"service": "media-service",
"timestamp": "2026-03-07T12:00:00.000Z"
}Access the professional admin UI at your worker URL:
https://your-worker.workers.dev/
Features:
- Drag-and-drop file upload
- Gallery view with thumbnails
- Filter by type and folder
- Pagination controls
- Copy URL to clipboard
- Delete with confirmation
- Real-time statistics
- Responsive design
Cloudflare automatically provides image transformations via the /cdn-cgi/image/ prefix:
# Resize to 800px width
https://cdn.yourdomain.com/cdn-cgi/image/width=800/images/blog/cat.png
# Thumbnail (200x200 crop)
https://cdn.yourdomain.com/cdn-cgi/image/width=200,height=200,fit=cover/images/blog/cat.png
# WebP conversion with quality
https://cdn.yourdomain.com/cdn-cgi/image/quality=75,format=webp/images/blog/cat.png
# Multiple transforms
https://cdn.yourdomain.com/cdn-cgi/image/width=400,quality=85,format=auto/images/blog/cat.png
See Cloudflare Image Resizing docs
// Upload a file
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('folder', 'blog');
formData.append('tags', 'tutorial,cloudflare');
const response = await fetch('https://your-worker.workers.dev/api/media/upload', {
method: 'POST',
headers: {
'x-api-key': 'YOUR_API_KEY'
},
body: formData
});
const { public_url } = await response.json();
console.log('Uploaded:', public_url);# Upload
curl -X POST https://your-worker.workers.dev/api/media/upload \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@image.png" \
-F "folder=blog"
# List
curl "https://your-worker.workers.dev/api/media/list?type=image&limit=20"
# Delete
curl -X DELETE https://your-worker.workers.dev/api/media/delete \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file_key":"images/blog/1741300000000-x7k3p-cat.png"}'import { useState } from 'react';
function ImageUpload() {
const [uploading, setUploading] = useState(false);
const [url, setUrl] = useState('');
const handleUpload = async (file: File) => {
setUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('folder', 'blog');
try {
const response = await fetch('https://your-worker.workers.dev/api/media/upload', {
method: 'POST',
headers: { 'x-api-key': process.env.MEDIA_API_KEY },
body: formData
});
const { public_url } = await response.json();
setUrl(public_url);
} finally {
setUploading(false);
}
};
return (
<div>
<input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
{uploading && <p>Uploading...</p>}
{url && <img src={url} alt="Uploaded" />}
</div>
);
}See COMMANDS.md for the complete command reference.
Quick reference:
npm run dev # Start local dev server
npm run deploy # Deploy to production
npm run typecheck # Run TypeScript type checking
npm run db:init # Initialize production database
npm run db:query # Query production database
npx wrangler tail # Stream production logsSee ENV.md for complete environment variable documentation.
Required variables:
ADMIN_API_KEY- API key for authenticated endpointsCDN_BASE_URL- R2 public domain for file URLsMAX_UPLOAD_BYTES- Maximum file size (default: 10MB)
-
Use local mode for faster iteration:
npm run dev
-
Monitor logs in real-time:
npx wrangler tail --format pretty
-
Query local database:
npx wrangler d1 execute media-db --local --command "SELECT * FROM media LIMIT 10" -
Test with the admin UI: Visit
http://localhost:8787/afternpm run dev
This project includes automated deployment via GitHub Actions.
-
Create GitHub repository:
git init git add . git commit -m "Initial commit" git remote add origin <your-repo-url> git push -u origin main
-
Get Cloudflare credentials:
npx wrangler whoami
Note your Account ID.
-
Create API token:
- Visit: https://dash.cloudflare.com/profile/api-tokens
- Create token with "Edit Cloudflare Workers" template
- Copy the token
-
Add GitHub secrets:
- Go to: Settings → Secrets → Actions
- Add
CLOUDFLARE_API_TOKENwith your API token - Add
CLOUDFLARE_ACCOUNT_IDwith your account ID
-
Push to trigger deployment:
git push origin main
The workflow will automatically:
- Run type checking
- Deploy to Cloudflare Workers
- Display the production URL
All errors follow a consistent format with proper HTTP status codes:
| Status | Error Class | Description |
|---|---|---|
| 400 | ValidationError |
Invalid input (bad request) |
| 401 | AuthenticationError |
Missing or invalid API key |
| 403 | AuthorizationError |
Insufficient permissions |
| 404 | NotFoundError |
Resource not found |
| 409 | ConflictError |
Resource conflict |
| 413 | ValidationError |
File too large |
| 415 | ValidationError |
Unsupported file type |
| 429 | RateLimitError |
Rate limit exceeded |
| 500 | StorageError |
R2 storage error |
| 500 | DatabaseError |
D1 database error |
| 500 | CacheError |
KV cache error (non-critical) |
Error response format:
{
"success": false,
"error": "File not found",
"code": "NOT_FOUND",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}In production, error details are hidden. In development, full stack traces are included.
Client Request
↓
Environment Validation Middleware
↓
Request ID Middleware (generates UUID, attaches logger)
↓
CORS Middleware
↓
Rate Limiting (if authenticated endpoint)
↓
Authentication (if required)
↓
Route Handler
↓
Service Layer (R2, D1, KV)
↓
Response with Request ID header
All operations are logged with structured JSON:
{
"level": "INFO",
"timestamp": "2026-03-07T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"message": "Upload completed successfully",
"context": {
"fileKey": "images/blog/cat.png",
"size": 120394
}
}Logs include request IDs for tracing across all service layers.
- Gallery queries: 5-minute TTL in KV
- Cache keys: Built from query params (type, folder, page, limit)
- Invalidation: Smart invalidation on upload/delete
- Fallback: Graceful degradation if cache fails
- API key authentication via
x-api-keyheader - Rate limiting: 60 requests/minute per IP
- Input validation on all endpoints
- MIME type whitelist
- File size limits
- No directory traversal (collision-safe naming)
1. "Database not initialized"
npx wrangler d1 execute media-db --file=schema.sql2. "API key required"
- Set local: Add to
.dev.vars - Set production:
npx wrangler secret put ADMIN_API_KEY
3. "CDN URL returns 404"
- Verify R2 bucket has custom domain configured
- Check
CDN_BASE_URLinwrangler.jsonc
4. "Upload fails with 413"
- Check
MAX_UPLOAD_BYTESenvironment variable - Default limit is 10MB
5. "Rate limit exceeded"
- Default: 60 requests/minute
- Adjust
RATE_LIMIT_PER_MINinwrangler.jsonc
See COMMANDS.md for more solutions.
# Stream logs in real-time
npx wrangler tail
# Pretty format
npx wrangler tail --format pretty
# Filter by status
npx wrangler tail --status error# View deployment info
npx wrangler deployments list
# View metrics
npx wrangler metrics# Check total files
npx wrangler d1 execute media-db --command "SELECT COUNT(*) FROM media"
# Recent uploads
npx wrangler d1 execute media-db --command "SELECT * FROM media ORDER BY created_at DESC LIMIT 10"- Upload latency: ~100-300ms (depends on file size)
- List queries (cached): ~10-50ms
- List queries (uncached): ~100-200ms
- Delete operations: ~50-150ms
- CDN delivery: Edge-cached globally
| Resource | Free Tier Limit |
|---|---|
| Worker requests | 100,000/day |
| Worker CPU time | 10ms/request |
| R2 storage | 10 GB |
| R2 Class A ops | 1M/month |
| R2 Class B ops | 10M/month |
| D1 storage | 5 GB |
| D1 reads | 5M/day |
| KV storage | 1 GB |
| KV reads | 100,000/day |
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Update documentation
- Submit a pull request
MIT License - see LICENSE file for details
- Documentation: COMMANDS.md | ENV.md
- Issues: GitHub Issues
- Cloudflare Docs: https://developers.cloudflare.com/workers/
Built with: