Files
gh-greyhaven-ai-claude-code…/skills/project-scaffolding/examples/cloudflare-worker-scaffold-example.md
2025-11-29 18:29:07 +08:00

14 KiB

Cloudflare Worker API Scaffold Example

Complete example of scaffolding a production-ready Cloudflare Workers API with Hono, TypeScript, D1 database, and comprehensive testing.

Duration: 15 minutes Files Created: 18 files Lines of Code: ~450 LOC Stack: Cloudflare Workers + Hono + TypeScript + D1 + Vitest


Complete File Tree

my-worker-api/
├── src/
│   ├── index.ts                 # Main entry point with Hono app
│   ├── routes/
│   │   ├── health.ts            # Health check endpoint
│   │   ├── users.ts             # User CRUD endpoints
│   │   └── index.ts             # Route exports
│   ├── middleware/
│   │   ├── auth.ts              # JWT authentication
│   │   ├── cors.ts              # CORS configuration
│   │   ├── logger.ts            # Request logging
│   │   └── error-handler.ts    # Global error handling
│   ├── services/
│   │   └── user-service.ts      # Business logic
│   ├── types/
│   │   └── environment.d.ts     # TypeScript types for env
│   └── utils/
│       └── db.ts                # Database helpers
├── tests/
│   ├── health.test.ts
│   ├── users.test.ts
│   └── setup.ts                 # Test configuration
├── .github/
│   └── workflows/
│       └── deploy.yml           # CI/CD pipeline
├── wrangler.toml                # Cloudflare configuration
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── .gitignore
├── .env.example
└── README.md

Total: 18 files, ~450 lines of code


Generated Files

1. wrangler.toml (Cloudflare Configuration)

name = "my-worker-api"
main = "src/index.ts"
compatibility_date = "2024-01-15"
node_compat = true

[observability]
enabled = true

[[d1_databases]]
binding = "DB"
database_name = "my-worker-api-db"
database_id = ""  # Add your database ID

[env.production]
[[env.production.d1_databases]]
binding = "DB"
database_name = "my-worker-api-prod"
database_id = ""  # Add production database ID

[vars]
ENVIRONMENT = "development"

# Secrets (set via: wrangler secret put SECRET_NAME)
# JWT_SECRET
# API_KEY

2. package.json

{
  "name": "my-worker-api",
  "version": "1.0.0",
  "description": "Production Cloudflare Workers API",
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy",
    "deploy:production": "wrangler deploy --env production",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "lint": "eslint src --ext .ts",
    "format": "prettier --write \"src/**/*.ts\"",
    "typecheck": "tsc --noEmit",
    "d1:migrations": "wrangler d1 migrations list DB",
    "d1:migrate": "wrangler d1 migrations apply DB"
  },
  "dependencies": {
    "hono": "^4.0.0"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20240117.0",
    "@types/node": "^20.11.0",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "eslint": "^8.56.0",
    "prettier": "^3.2.4",
    "typescript": "^5.3.3",
    "vitest": "^1.2.0",
    "wrangler": "^3.25.0"
  }
}

3. src/index.ts (Main Entry Point)

import { Hono } from 'hono';
import { cors } from './middleware/cors';
import { logger } from './middleware/logger';
import { errorHandler } from './middleware/error-handler';
import { healthRoutes } from './routes/health';
import { userRoutes } from './routes/users';
import type { Environment } from './types/environment';

const app = new Hono<{ Bindings: Environment }>();

// Global middleware
app.use('*', cors());
app.use('*', logger());

// Routes
app.route('/health', healthRoutes);
app.route('/api/users', userRoutes);

// Error handling
app.onError(errorHandler);

// 404 handler
app.notFound((c) => {
  return c.json({ error: 'Not Found', path: c.req.path }, 404);
});

export default app;

4. src/routes/health.ts (Health Check)

import { Hono } from 'hono';
import type { Environment } from '../types/environment';

export const healthRoutes = new Hono<{ Bindings: Environment }>();

healthRoutes.get('/', async (c) => {
  const db = c.env.DB;

  try {
    // Check database connection
    const result = await db.prepare('SELECT 1 as health').first();

    return c.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      environment: c.env.ENVIRONMENT || 'unknown',
      database: result ? 'connected' : 'error',
    });
  } catch (error) {
    return c.json({
      status: 'unhealthy',
      timestamp: new Date().toISOString(),
      error: error instanceof Error ? error.message : 'Unknown error',
    }, 503);
  }
});

5. src/routes/users.ts (User CRUD)

import { Hono } from 'hono';
import { auth } from '../middleware/auth';
import { UserService } from '../services/user-service';
import type { Environment } from '../types/environment';

export const userRoutes = new Hono<{ Bindings: Environment }>();

// List users (requires auth)
userRoutes.get('/', auth(), async (c) => {
  const userService = new UserService(c.env.DB);
  const users = await userService.listUsers();

  return c.json({ users });
});

// Get user by ID
userRoutes.get('/:id', auth(), async (c) => {
  const id = c.req.param('id');
  const userService = new UserService(c.env.DB);
  const user = await userService.getUserById(id);

  if (!user) {
    return c.json({ error: 'User not found' }, 404);
  }

  return c.json({ user });
});

// Create user
userRoutes.post('/', auth(), async (c) => {
  const body = await c.req.json();
  const userService = new UserService(c.env.DB);

  // Validate input
  if (!body.email || !body.name) {
    return c.json({ error: 'Email and name are required' }, 400);
  }

  const user = await userService.createUser(body);
  return c.json({ user }, 201);
});

// Update user
userRoutes.put('/:id', auth(), async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json();
  const userService = new UserService(c.env.DB);

  const user = await userService.updateUser(id, body);

  if (!user) {
    return c.json({ error: 'User not found' }, 404);
  }

  return c.json({ user });
});

// Delete user
userRoutes.delete('/:id', auth(), async (c) => {
  const id = c.req.param('id');
  const userService = new UserService(c.env.DB);

  const deleted = await userService.deleteUser(id);

  if (!deleted) {
    return c.json({ error: 'User not found' }, 404);
  }

  return c.json({ message: 'User deleted successfully' });
});

6. src/middleware/auth.ts (JWT Authentication)

import { createMiddleware } from 'hono/factory';
import type { Environment } from '../types/environment';

export const auth = () => {
  return createMiddleware<{ Bindings: Environment }>(async (c, next) => {
    const authHeader = c.req.header('Authorization');

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ error: 'Unauthorized' }, 401);
    }

    const token = authHeader.substring(7);

    // Verify JWT (simplified - use proper JWT library in production)
    if (token !== c.env.JWT_SECRET) {
      return c.json({ error: 'Invalid token' }, 401);
    }

    // Add user info to context
    c.set('user', { id: 'user-123', email: 'user@example.com' });

    await next();
  });
};

7. src/middleware/cors.ts (CORS Configuration)

import { createMiddleware } from 'hono/factory';

export const cors = () => {
  return createMiddleware(async (c, next) => {
    await next();

    c.header('Access-Control-Allow-Origin', '*');
    c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

    if (c.req.method === 'OPTIONS') {
      return c.text('', 204);
    }
  });
};

8. src/middleware/logger.ts (Request Logging)

import { createMiddleware } from 'hono/factory';

export const logger = () => {
  return createMiddleware(async (c, next) => {
    const start = Date.now();
    const method = c.req.method;
    const path = c.req.path;

    await next();

    const duration = Date.now() - start;
    const status = c.res.status;

    console.log(`${method} ${path} ${status} ${duration}ms`);
  });
};

9. src/middleware/error-handler.ts (Global Error Handling)

import type { ErrorHandler } from 'hono';

export const errorHandler: ErrorHandler = (err, c) => {
  console.error('Error:', err);

  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';

  return c.json(
    {
      error: message,
      ...(c.env.ENVIRONMENT === 'development' && { stack: err.stack }),
    },
    status
  );
};

10. src/services/user-service.ts (Business Logic)

import type { D1Database } from '@cloudflare/workers-types';

interface User {
  id: string;
  email: string;
  name: string;
  created_at: string;
}

export class UserService {
  constructor(private db: D1Database) {}

  async listUsers(): Promise<User[]> {
    const result = await this.db
      .prepare('SELECT * FROM users ORDER BY created_at DESC')
      .all<User>();

    return result.results || [];
  }

  async getUserById(id: string): Promise<User | null> {
    const result = await this.db
      .prepare('SELECT * FROM users WHERE id = ?')
      .bind(id)
      .first<User>();

    return result;
  }

  async createUser(data: { email: string; name: string }): Promise<User> {
    const id = crypto.randomUUID();
    const now = new Date().toISOString();

    await this.db
      .prepare('INSERT INTO users (id, email, name, created_at) VALUES (?, ?, ?, ?)')
      .bind(id, data.email, data.name, now)
      .run();

    return {
      id,
      email: data.email,
      name: data.name,
      created_at: now,
    };
  }

  async updateUser(id: string, data: Partial<{ email: string; name: string }>): Promise<User | null> {
    const existing = await this.getUserById(id);
    if (!existing) return null;

    const updates: string[] = [];
    const values: any[] = [];

    if (data.email !== undefined) {
      updates.push('email = ?');
      values.push(data.email);
    }
    if (data.name !== undefined) {
      updates.push('name = ?');
      values.push(data.name);
    }

    if (updates.length > 0) {
      values.push(id);
      await this.db
        .prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`)
        .bind(...values)
        .run();
    }

    return this.getUserById(id);
  }

  async deleteUser(id: string): Promise<boolean> {
    const result = await this.db
      .prepare('DELETE FROM users WHERE id = ?')
      .bind(id)
      .run();

    return result.success;
  }
}

11. src/types/environment.d.ts (TypeScript Types)

export interface Environment {
  DB: D1Database;
  JWT_SECRET: string;
  API_KEY: string;
  ENVIRONMENT: string;
}

12. tests/health.test.ts (Health Check Tests)

import { describe, it, expect } from 'vitest';
import app from '../src/index';

describe('Health Check', () => {
  it('should return healthy status', async () => {
    const req = new Request('http://localhost/health');
    const res = await app.fetch(req, {
      DB: getMockDB(),
      ENVIRONMENT: 'test',
    } as any);

    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data).toHaveProperty('status', 'healthy');
    expect(data).toHaveProperty('timestamp');
  });
});

function getMockDB() {
  return {
    prepare: () => ({
      first: async () => ({ health: 1 }),
    }),
  };
}

13. README.md (Documentation)

# My Worker API

Production-ready Cloudflare Workers API with TypeScript, Hono, and D1 database.

## Quick Start

\`\`\`bash
# Install dependencies
npm install

# Create D1 database
wrangler d1 create my-worker-api-db

# Update wrangler.toml with database ID

# Run migrations
npm run d1:migrate

# Start development server
npm run dev

# Open http://localhost:8787/health
\`\`\`

## API Endpoints

- `GET /health` - Health check
- `GET /api/users` - List users (requires auth)
- `GET /api/users/:id` - Get user by ID (requires auth)
- `POST /api/users` - Create user (requires auth)
- `PUT /api/users/:id` - Update user (requires auth)
- `DELETE /api/users/:id` - Delete user (requires auth)

## Authentication

Include Bearer token in Authorization header:

\`\`\`bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8787/api/users
\`\`\`

## Deployment

\`\`\`bash
# Deploy to production
npm run deploy:production

# Set secrets
wrangler secret put JWT_SECRET
wrangler secret put API_KEY
\`\`\`

## Testing

\`\`\`bash
# Run tests
npm test

# With coverage
npm run test:coverage
\`\`\`

Scaffold Process

Step 1: Initialize (2 minutes)

mkdir my-worker-api && cd my-worker-api
npm init -y
npm install hono
npm install -D @cloudflare/workers-types typescript wrangler vitest

Step 2: Generate Configuration (3 minutes)

  • Create wrangler.toml
  • Create tsconfig.json
  • Create package.json scripts
  • Create .gitignore

Step 3: Generate Source Code (5 minutes)

  • Create src/index.ts
  • Create routes/
  • Create middleware/
  • Create services/
  • Create types/

Step 4: Generate Tests (3 minutes)

  • Create tests/ directory
  • Create test files
  • Create test setup

Step 5: Generate CI/CD (2 minutes)

  • Create .github/workflows/deploy.yml
  • Create README.md
  • Create .env.example

Next Steps

After scaffolding:

  1. Update database ID in wrangler.toml
  2. Run migrations: npm run d1:migrate
  3. Set secrets: wrangler secret put JWT_SECRET
  4. Test locally: npm run dev
  5. Deploy: npm run deploy:production

Total Time: 15 minutes Total Files: 18 Total LOC: ~450 Ready for: Production deployment