← Home

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

2. D1 Database Features

3. Developer Experience

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();

// DANGEROUS - never do this // const result = await env.DB.prepare( // SELECT FROM posts WHERE slug = '${userInput}' // ).first();

return 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 --local

Run migrations locally

wrangler d1 execute my-database --local --file=schema.sql

Start development server

wrangler dev

Deploy to production

wrangler deploy

Resources

Real-World Use Cases

This stack is perfect for:

Tags

serverless, cloudflare, workers, d1, typescript, edge-computing, sqlite, web-development

--- Published on 2026-04-18