Skip to content

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

Compatibility Notes

Cloudflare Workers use the V8 JavaScript runtime, not Node.js. This means:

FeatureStatus
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

  1. Install Wrangler CLI

    Terminal window
    npm install -g wrangler
  2. Login to Cloudflare

    Terminal window
    wrangler login
  3. 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 Workers
type Bindings = {
TURSO_DATABASE_URL: string;
TURSO_AUTH_TOKEN: string;
CORS_ORIGIN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
// CORS middleware
app.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 check
app.get('/health', (c) => {
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
});
// Example: Get threads
app.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 ID
app.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 Workers
export 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 hour

Add Secrets

Terminal window
# Add Turso credentials as secrets
wrangler secret put TURSO_DATABASE_URL
# Enter: libsql://your-db.turso.io
wrangler secret put TURSO_AUTH_TOKEN
# Enter: your-auth-token

Deployment

Development

Terminal window
# Start local dev server
wrangler dev
# Test at http://localhost:8787

Production

Terminal window
# Deploy to Cloudflare
wrangler deploy
# Output:
# Deployed discord-forum-api (https://discord-forum-api.your-subdomain.workers.dev)

Custom Domain

  1. In Cloudflare Dashboard, go to Workers & Pages
  2. Select your worker
  3. Go to SettingsTriggers
  4. Add Custom Domain (e.g., api.yourdomain.com)
  5. 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

Terminal window
wrangler kv:namespace create "CACHE"
# Add the output to wrangler.toml

Configure 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:

LimitFreePaid ($5/mo)
Requests/day100,00010 million
CPU time10ms50ms
Memory128 MB128 MB
Subrequest501,000
Script size1 MB10 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 handler
app.notFound((c) => {
return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);
});

Logging

Use console.log for basic logging (visible in Wrangler tail):

Terminal window
# View live logs
wrangler tail

For 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:

Terminal window
# Test locally first
wrangler dev

Turso Connection Errors

  1. Verify secrets are set:

    Terminal window
    wrangler secret list
  2. Test Turso connection locally:

    Terminal window
    turso db shell your-db
  3. 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 pagination
app.get('/api/threads', async (c) => {
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 100);
// ... fetch with limit
});