Initial commit
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
# 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)
|
||||
|
||||
```toml
|
||||
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
|
||||
|
||||
```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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
export interface Environment {
|
||||
DB: D1Database;
|
||||
JWT_SECRET: string;
|
||||
API_KEY: string;
|
||||
ENVIRONMENT: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 12. tests/health.test.ts (Health Check Tests)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```markdown
|
||||
# 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)
|
||||
|
||||
```bash
|
||||
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
|
||||
Reference in New Issue
Block a user