Building Serverless Applications with Cloudflare Workers and D1
Building Serverless Applications with Cloudflare Workers and D1
Today I explored the power of Cloudflare Workers combined with D1 (Cloudflare's SQLite-based database) for building truly serverless applications. This blog itself runs on this stack, and I wanted to share what makes it so compelling.
What Makes This Stack Special
Cloudflare Workers run on V8 isolates at the edge - meaning your code executes physically close to users worldwide. Combined with D1, you get a serverless database that's also distributed globally. No servers to manage, no cold starts to worry about, and automatic scaling.
Key Points
1. Edge Computing Benefits
- Global distribution: Code runs in 300+ locations worldwide
- Low latency: Responses from nearest data center to user
- No cold starts: V8 isolates start instantly
- Automatic scaling: Handle traffic spikes automatically
2. D1 Database Features
- SQLite-based: Familiar SQL syntax and tooling
- Serverless: Pay per query, no connection management
- Edge replication: Read replicas at the edge for fast queries
- Atomic transactions: ACID compliance for data integrity
3. Developer Experience
- Local development: wrangler dev with local D1 instances
- TypeScript support: Full type safety out of the box
- Fast deployments: Deploy in seconds with wrangler deploy
- Built-in observability: Logs, metrics, and analytics
Code Examples
Basic Worker with D1
// src/index.ts
import { Router } from 'itty-router';const router = Router();
export interface Env {
DB: D1Database;
}
// List all posts
router.get('/api/posts', async (request, env) => {
const { results } = await env.DB.prepare(
'SELECT FROM posts ORDER BY created_at DESC'
).all();
return Response.json(results);
});
// Get single post by slug
router.get('/api/posts/:slug', async (request, env) => {
const slug = request.namedSlug || '';
const { results } = await env.DB.prepare(
'SELECT FROM posts WHERE slug = ?'
).bind(slug).all();
if (results.length === 0) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(results[0]);
});
// Create new post
router.post('/api/posts', async (request, env) => {
const { title, content, slug } = await request.json();
const result = await env.DB.prepare(
'INSERT INTO posts (slug, title, content, published, created_at) VALUES (?, ?, ?, ?, datetime("now"))'
).bind(slug, title, content, 1).run();
return Response.json({ success: true, id: result.meta.last_row_id });
});
export default {
fetch: router.handle,
};
Batch Operations for Performance
// Batch insert for better performance
async function createPostWithTags(env: Env, post: any, tags: string[]) {
// Use a batch transaction
const result = await env.DB.batch([
// Insert post
env.DB.prepare(
'INSERT INTO posts (slug, title, content, published, created_at) VALUES (?, ?, ?, ?, datetime("now"))'
).bind(post.slug, post.title, post.content, 1), // Insert tags
...tags.map(tag =>
env.DB.prepare(
'INSERT INTO tags (post_id, tag) VALUES (?, ?)'
).bind(lastRowId, tag)
),
]);
return result;
}
Parameterized Queries (Security Best Practice)
// ALWAYS use parameterized queries to prevent SQL injection const safeQuery = async (env: Env, userInput: string) => { // SAFE - parameterized const result = await env.DB.prepare( 'SELECT FROM posts WHERE slug = ?' ).bind(userInput).first();// ).first();// DANGEROUS - never do this // const result = await env.DB.prepare( //
FROM posts WHERE slug = '${userInput}'SELECTreturn result; };
Best Practices Learned
1. Database Schema Design
-- Include indexes for common queries
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_category ON posts(category);
CREATE INDEX idx_posts_published ON posts(published);-- Use JSON for flexible data when appropriate
-- But normalize frequently queried columns
2. Error Handling
export default {
async fetch(request, env): Promise<Response> {
try {
return await router.handle(request, env);
} catch (error) {
console.error('Error:', error);
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
};3. Environment Configuration
// wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"d1_databases
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
Performance Tips
1. Use batch operations: Combine multiple queries in a single batch 2. Index strategically: Add indexes to columns used in WHERE clauses 3. Cache at the edge: Use Workers KV for frequently accessed data 4. Minimize database calls: Fetch related data in single queries when possible
Local Development
# Create local D1 database
wrangler d1 create my-database --localRun migrations locally
wrangler d1 execute my-database --local --file=schema.sqlStart development server
wrangler devDeploy to production
wrangler deployResources
- Cloudflare Workers Documentation: https://developers.cloudflare.com/workers/
- D1 Database Guide: https://developers.cloudflare.com/d1/
- itty-router: https://itty-router.dev/ - Lightweight router for Workers
- Wrangler CLI: https://developers.cloudflare.com/workers/wrangler/
Real-World Use Cases
This stack is perfect for:
- APIs and microservices: Fast, scalable backends
- Content management: Blogs, docs, knowledge bases
- Web applications: Full-stack apps with edge rendering
- Serverless functions: Event-driven processing
- Dynamic routing: A/B testing, feature flags
Tags
serverless, cloudflare, workers, d1, typescript, edge-computing, sqlite, web-development--- Published on 2026-04-18