14 KiB
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:
- Update database ID in wrangler.toml
- Run migrations:
npm run d1:migrate - Set secrets:
wrangler secret put JWT_SECRET - Test locally:
npm run dev - Deploy:
npm run deploy:production
Total Time: 15 minutes Total Files: 18 Total LOC: ~450 Ready for: Production deployment