Why I Chose Cloudflare Workers for My Portfolio API
When I needed a backend for my portfolio site, I evaluated several options: Vercel Functions, AWS Lambda, Railway, and Cloudflare Workers. I chose Workers, and here is why.
Why Cloudflare Workers?
| Feature | Workers | Lambda | Vercel Functions |
|---|---|---|---|
| Cold start | 0ms | 100-500ms | 50-200ms |
| Free tier | 100K req/day | 1M req/month | 100K/month |
| Global edge | Yes | Regional | Edge (limited) |
| Custom domains | Free | Via API GW | Via Vercel |
| TypeScript | Native | Via build | Native |
The zero cold start was the deciding factor. My API responses are consistently fast regardless of traffic patterns.
Project Structure
src/
├── domain/
│ ├── entities/ # BlogPost, Project, User, PageView
│ └── repositories/ # Interface contracts
├── application/
│ └── usecases/ # Business logic
├── infrastructure/
│ └── supabase/ # Supabase implementations
└── interfaces/
└── http/
├── handlers/ # Request handlers
├── middleware/ # Auth, CORS, validation
└── modular-router.ts # Declarative routing
Declarative Routing
Instead of an Express-style router, I built a declarative route table:
1const routes: RouteDefinition[] = [
2 // Public routes
3 { method: 'GET', paths: ['/v1/posts'], handler: (req) => blogHandler.getPosts(req), auth: 'none' },
4 { method: 'GET', paths: ['/v1/projects/featured'], handler: (req) => projectHandler.getFeatured(req), auth: 'none' },
5
6 // Authenticated routes
7 { method: 'POST', paths: ['/v1/posts'], handler: (req) => blogHandler.createPost(req), auth: 'authenticate' },
8 { method: 'PUT', paths: ['/v1/posts/:id'], handler: (req, p) => blogHandler.updatePost(p, req), auth: 'authenticate' },
9
10 // Check-only routes (optional auth)
11 { method: 'DELETE', paths: ['/v1/posts/:id'], handler: (_, p) => blogHandler.deletePost(p), auth: 'check' },
12];The router matches paths, extracts params, handles CORS, and dispatches to the correct auth middleware — all from this single configuration.
Authentication Middleware
JWT verification via Supabase:
1class AuthMiddleware {
2 async authenticate(request: Request): Promise<AuthenticatedRequest> {
3 const token = request.headers.get('Authorization')?.replace('Bearer ', '');
4 if (!token) throw new Error('No token');
5
6 const { data: { user }, error } = await supabase.auth.getUser(token);
7 if (error || !user) throw new Error('Invalid token');
8
9 (request as AuthenticatedRequest).user = {
10 id: user.id,
11 email: user.email!,
12 };
13 return request as AuthenticatedRequest;
14 }
15}Deployment
Deployment is a single command:
1npx wrangler deployWrangler handles bundling, uploading, and routing. My API is live at api.iletai.qzz.io with a custom domain configured through Cloudflare DNS.
Environment Variables
1// wrangler.jsonc
2{
3 "name": "technical-backend-worker",
4 "main": "src/index.ts",
5 "compatibility_date": "2024-09-23",
6 "vars": {
7 "SUPABASE_URL": "https://xxx.supabase.co"
8 }
9}Secrets are stored via wrangler secret put SUPABASE_ANON_KEY.
Performance Results
After deploying globally:
- P50 latency: 12ms (from Vietnam)
- P50 latency: 25ms (from US East)
- P99 latency: 45ms
- Zero cold starts: consistent performance
Conclusion
Cloudflare Workers is an excellent choice for API backends. The zero cold starts, global edge deployment, generous free tier, and native TypeScript support make it hard to beat. Combined with Supabase for data and auth, you get a full-stack serverless architecture at minimal cost.