Cloudflare Workers
Deploy the API to Cloudflare’s edge network for ultra-low latency.
Why Cloudflare Workers?
- Edge deployment - Run in 300+ locations worldwide
- No cold starts - Near-instant response times
- Generous free tier - 100,000 requests/day free
- Turso compatible - Works great with edge databases
- Zero configuration - No servers to manage
Prerequisites
- Cloudflare account (free)
- Turso database (free tier available)
- Bot deployed elsewhere (Railway, VPS, etc.)
- Node.js 18+ installed
Compatibility Notes
Cloudflare Workers use the V8 JavaScript runtime, not Node.js. This means:
| Feature | Status |
|---|---|
| Hono.js | ✅ Works |
| Drizzle ORM | ✅ Works |
| Turso/libSQL | ✅ Works |
| Node.js APIs | ⚠️ Limited |
| File system | ❌ Not available |
| SQLite (file) | ❌ Not available |
You must use Turso (or another edge-compatible database) instead of local SQLite.
Project Setup
-
Install Wrangler CLI
Terminal window npm install -g wrangler -
Login to Cloudflare
Terminal window wrangler login -
Create Workers project
In your API package, create
wrangler.toml:name = "discord-forum-api"main = "src/worker.ts"compatibility_date = "2024-01-01"[vars]DATABASE_TYPE = "turso"# Secrets (add via wrangler secret put)# TURSO_DATABASE_URL# TURSO_AUTH_TOKEN
Adapting the API
Create Worker Entry Point
Create packages/api/src/worker.ts:
import { Hono } from 'hono';import { cors } from 'hono/cors';import { createClient } from '@libsql/client/web';
// Types for Cloudflare Workerstype Bindings = { TURSO_DATABASE_URL: string; TURSO_AUTH_TOKEN: string; CORS_ORIGIN: string;};
const app = new Hono<{ Bindings: Bindings }>();
// CORS middlewareapp.use('*', async (c, next) => { const corsMiddleware = cors({ origin: c.env.CORS_ORIGIN || '*', credentials: true, }); return corsMiddleware(c, next);});
// Database connection (per-request)function getDb(env: Bindings) { return createClient({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN, });}
// Health checkapp.get('/health', (c) => { return c.json({ status: 'ok', runtime: 'cloudflare-workers' });});
// Example: Get threadsapp.get('/api/threads', async (c) => { const db = getDb(c.env); const serverId = c.req.query('serverId'); const limit = parseInt(c.req.query('limit') || '20');
const result = await db.execute({ sql: `SELECT * FROM threads WHERE server_id = ? ORDER BY created_at DESC LIMIT ?`, args: [serverId, limit], });
return c.json({ threads: result.rows });});
// Example: Get thread by IDapp.get('/api/threads/:id', async (c) => { const db = getDb(c.env); const threadId = c.req.param('id');
const thread = await db.execute({ sql: `SELECT * FROM threads WHERE id = ?`, args: [threadId], });
if (thread.rows.length === 0) { return c.json({ error: 'Thread not found' }, 404); }
const messages = await db.execute({ sql: `SELECT * FROM messages WHERE thread_id = ? ORDER BY created_at`, args: [threadId], });
return c.json({ ...thread.rows[0], messages: messages.rows, });});
// Export for Cloudflare Workersexport default app;Update package.json
Add worker build script to packages/api/package.json:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "worker:dev": "wrangler dev src/worker.ts", "worker:deploy": "wrangler deploy src/worker.ts" }}Configuration
wrangler.toml
Full configuration:
name = "discord-forum-api"main = "src/worker.ts"compatibility_date = "2024-01-01"compatibility_flags = ["nodejs_compat"]
[vars]DATABASE_TYPE = "turso"CORS_ORIGIN = "https://yourdomain.com"
# Optional: Custom domain# routes = [# { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }# ]
# Optional: Cron triggers for background tasks# [triggers]# crons = ["0 * * * *"] # Every hourAdd Secrets
# Add Turso credentials as secretswrangler secret put TURSO_DATABASE_URL# Enter: libsql://your-db.turso.io
wrangler secret put TURSO_AUTH_TOKEN# Enter: your-auth-tokenDeployment
Development
# Start local dev serverwrangler dev
# Test at http://localhost:8787Production
# Deploy to Cloudflarewrangler deploy
# Output:# Deployed discord-forum-api (https://discord-forum-api.your-subdomain.workers.dev)Custom Domain
- In Cloudflare Dashboard, go to Workers & Pages
- Select your worker
- Go to Settings → Triggers
- Add Custom Domain (e.g.,
api.yourdomain.com) - DNS is configured automatically
Caching
Add edge caching for better performance:
app.get('/api/threads', async (c) => { // Check cache first const cacheKey = new Request(c.req.url); const cache = caches.default;
let response = await cache.match(cacheKey); if (response) { return new Response(response.body, { ...response, headers: { ...response.headers, 'X-Cache': 'HIT' }, }); }
// Fetch from database const db = getDb(c.env); const result = await db.execute({ sql: 'SELECT * FROM threads LIMIT 20' });
response = c.json({ threads: result.rows });
// Cache for 60 seconds response.headers.set('Cache-Control', 's-maxage=60');
// Store in cache c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()));
return response;});KV Storage (Optional)
Use Cloudflare KV for caching:
Create KV Namespace
wrangler kv:namespace create "CACHE"# Add the output to wrangler.tomlConfigure in wrangler.toml
[[kv_namespaces]]binding = "CACHE"id = "your-namespace-id"Use in Code
type Bindings = { CACHE: KVNamespace; // ... other bindings};
app.get('/api/stats/:serverId', async (c) => { const serverId = c.req.param('serverId');
// Check KV cache const cached = await c.env.CACHE.get(`stats:${serverId}`); if (cached) { return c.json(JSON.parse(cached)); }
// Fetch from database const stats = await fetchStats(serverId);
// Cache for 5 minutes await c.env.CACHE.put(`stats:${serverId}`, JSON.stringify(stats), { expirationTtl: 300, });
return c.json(stats);});Limits
Be aware of Workers limits:
| Limit | Free | Paid ($5/mo) |
|---|---|---|
| Requests/day | 100,000 | 10 million |
| CPU time | 10ms | 50ms |
| Memory | 128 MB | 128 MB |
| Subrequest | 50 | 1,000 |
| Script size | 1 MB | 10 MB |
Error Handling
app.onError((err, c) => { console.error('Worker error:', err);
// Don't expose internal errors if (err instanceof HTTPException) { return err.getResponse(); }
return c.json( { error: 'Internal server error', code: 'INTERNAL_ERROR' }, 500 );});
// 404 handlerapp.notFound((c) => { return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);});Logging
Use console.log for basic logging (visible in Wrangler tail):
# View live logswrangler tailFor structured logging:
function log(level: string, message: string, data?: object) { console.log(JSON.stringify({ level, message, ...data, timestamp: Date.now() }));}
app.use('*', async (c, next) => { const start = Date.now(); await next(); log('info', 'request', { method: c.req.method, path: c.req.path, status: c.res.status, duration: Date.now() - start, });});Troubleshooting
”Module not found” Errors
Workers don’t support all Node.js modules. Check compatibility:
# Test locally firstwrangler devTurso Connection Errors
-
Verify secrets are set:
Terminal window wrangler secret list -
Test Turso connection locally:
Terminal window turso db shell your-db -
Check URL format is
libsql://...
CPU Time Exceeded
- Optimize database queries
- Add pagination
- Use caching
- Upgrade to paid plan for 50ms limit
Large Response Errors
Workers have response size limits. For large datasets:
// Use paginationapp.get('/api/threads', async (c) => { const limit = Math.min(parseInt(c.req.query('limit') || '20'), 100); // ... fetch with limit});