Cloud Functions
The cloud functions are serverless edge functions deployed on Cloudflare Workers, handling specific tasks like file processing and JWT operations.
Technology Stack
- Cloudflare Workers - Serverless edge computing platform
- Hono - Lightweight web framework
- Wrangler - Cloudflare Workers CLI tool
- TypeScript - Type-safe JavaScript
Project Structure
apps/cloud-functions/
├── src/
│ ├── functions/ # Individual cloud functions
│ │ ├── analytics.ts
│ │ ├── fileProcessor.ts
│ │ └── ...
│ │
│ ├── utils/ # Utility functions
│ │ └── ...
│ │
│ ├── middleware.ts # Hono middleware
│ ├── router.ts # Route definitions
│ ├── jwks.ts # JWT key set handling
│ ├── types.ts # Type definitions
│ └── index.ts # Entry point
│
├── wrangler.toml # Cloudflare Workers configuration
├── tsconfig.json # TypeScript configuration
└── package.jsonArchitecture
Cloud Functions run at the edge on Cloudflare's global network, providing low-latency access to specific operations.
Entry Point
typescript
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { router } from "./router";
import type { Env } from "./types";
const app = new Hono<{ Bindings: Env }>();
// Middleware
app.use(
"*",
cors({
origin: (origin) => origin,
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);
// Mount routes
app.route("/", router);
// Health check
app.get("/health", (c) => c.json({ status: "ok" }));
export default app;Router Configuration
typescript
// src/router.ts
import { Hono } from "hono";
import { authMiddleware } from "./middleware";
import { fileProcessor } from "./functions/fileProcessor";
import { jwtUtil } from "./functions/jwtUtil";
import type { Env } from "./types";
export const router = new Hono<{ Bindings: Env }>();
// Public endpoints
router.get("/.well-known/jwks.json", jwtUtil.getJWKS);
// Protected endpoints
router.use("/api/*", authMiddleware);
router.post("/api/process-file", fileProcessor.processFile);
router.post("/api/generate-jwt", jwtUtil.generateToken);
router.post("/api/verify-jwt", jwtUtil.verifyToken);Middleware
Authentication Middleware
typescript
// src/middleware.ts
import { Context, Next } from "hono";
import { verify } from "hono/jwt";
import type { Env } from "./types";
export async function authMiddleware(
c: Context<{ Bindings: Env }>,
next: Next,
) {
const authHeader = c.req.header("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return c.json({ error: "Unauthorized" }, 401);
}
const token = authHeader.substring(7);
try {
const payload = await verify(token, c.env.JWT_SECRET);
c.set("user", payload);
await next();
} catch (error) {
return c.json({ error: "Invalid token" }, 401);
}
}
export async function corsMiddleware(
c: Context<{ Bindings: Env }>,
next: 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");
}Functions
File Processing
typescript
// src/functions/fileProcessor.ts
import { Context } from "hono";
import AdmZip from "adm-zip";
import type { Env } from "../types";
export const fileProcessor = {
async processFile(c: Context<{ Bindings: Env }>) {
const body = await c.req.parseBody();
const file = body.file as File;
if (!file) {
return c.json({ error: "No file provided" }, 400);
}
try {
// Example: Extract zip file
const buffer = await file.arrayBuffer();
const zip = new AdmZip(Buffer.from(buffer));
const entries = zip.getEntries();
const files = entries.map((entry) => ({
name: entry.entryName,
size: entry.header.size,
isDirectory: entry.isDirectory,
}));
return c.json({
success: true,
fileCount: files.length,
files,
});
} catch (error) {
return c.json(
{
error: "Failed to process file",
details: error.message,
},
500,
);
}
},
};JWT Utilities
typescript
// src/functions/jwtUtil.ts
import { Context } from "hono";
import { sign, verify } from "hono/jwt";
import type { Env } from "../types";
import { getJWKS } from "../jwks";
export const jwtUtil = {
async generateToken(c: Context<{ Bindings: Env }>) {
const body = await c.req.json();
const { userId, email, role } = body;
if (!userId || !email) {
return c.json({ error: "Missing required fields" }, 400);
}
const payload = {
sub: userId,
email,
role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 24 hours
};
const token = await sign(payload, c.env.JWT_SECRET);
return c.json({ token });
},
async verifyToken(c: Context<{ Bindings: Env }>) {
const body = await c.req.json();
const { token } = body;
if (!token) {
return c.json({ error: "Token required" }, 400);
}
try {
const payload = await verify(token, c.env.JWT_SECRET);
return c.json({ valid: true, payload });
} catch (error) {
return c.json({ valid: false, error: error.message }, 401);
}
},
async getJWKS(c: Context<{ Bindings: Env }>) {
// Return JSON Web Key Set for token verification
const jwks = getJWKS(c.env.JWT_SECRET);
return c.json(jwks);
},
};JWKS Implementation
typescript
// src/jwks.ts
import { createPublicKey } from "crypto";
export function getJWKS(secret: string) {
// Generate JWKS from secret
// This is a simplified example - in production, use proper key management
return {
keys: [
{
kty: "oct",
kid: "default",
use: "sig",
alg: "HS256",
// In production, don't expose the secret!
// Use proper public/private key pairs
},
],
};
}Environment Variables
Environment variables are configured in Wrangler:
toml
# wrangler.toml
name = "lms-cloud-functions"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
# Non-sensitive variables
[env.production]
vars = { ENVIRONMENT = "production" }
[env.development]
vars = { ENVIRONMENT = "development" }Secret variables are stored separately:
bash
# Set secrets using Wrangler
wrangler secret put JWT_SECRET
wrangler secret put API_KEYLocal development (.dev.vars):
env
JWT_SECRET=your_local_jwt_secret
API_KEY=your_local_api_keyType Definitions
typescript
// src/types.ts
export interface Env {
// Environment variables
JWT_SECRET: string;
ENVIRONMENT: "development" | "production";
// KV namespaces (if using)
CACHE: KVNamespace;
// Durable Objects (if using)
// COUNTER: DurableObjectNamespace;
}
export interface User {
id: number;
email: string;
role: string;
}
export interface JWTPayload {
sub: number;
email: string;
role: string;
iat: number;
exp: number;
}Development
Local Development
bash
# Start development server
pnpm dev
# This runs:
wrangler devThe dev server runs at http://localhost:8787 by default.
Testing Locally
bash
# Test health check
curl http://localhost:8787/health
# Test JWT generation
curl -X POST http://localhost:8787/api/generate-jwt \
-H "Content-Type: application/json" \
-d '{"userId": 1, "email": "test@example.com", "role": "student"}'
# Test JWT verification
curl -X POST http://localhost:8787/api/verify-jwt \
-H "Content-Type: application/json" \
-d '{"token": "your.jwt.token"}'Deployment
Deploy to Cloudflare
bash
# Deploy to production
pnpm deploy
# This runs:
wrangler deploy
# Deploy to staging
wrangler deploy --env stagingDeployment Configuration
toml
# wrangler.toml
[env.production]
name = "lms-cloud-functions"
routes = [
{ pattern = "functions.example.com/*", zone_name = "example.com" }
]
[env.staging]
name = "lms-cloud-functions-staging"
routes = [
{ pattern = "functions-staging.example.com/*", zone_name = "example.com" }
]Performance Considerations
Edge Caching
typescript
// Cache responses at the edge
app.get("/api/data", async (c) => {
const cacheKey = new Request(c.req.url, c.req.raw);
const cache = caches.default;
// Check cache
let response = await cache.match(cacheKey);
if (!response) {
// Fetch data
const data = await fetchData();
response = new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300", // 5 minutes
},
});
// Store in cache
await cache.put(cacheKey, response.clone());
}
return response;
});KV Storage
For persistent storage across edge locations:
typescript
// Store data in KV
await c.env.CACHE.put("key", JSON.stringify(data), {
expirationTtl: 3600, // 1 hour
});
// Retrieve from KV
const cached = await c.env.CACHE.get("key", "json");Error Handling
typescript
// Global error handler
app.onError((err, c) => {
console.error("Error:", err);
return c.json(
{
error: "Internal server error",
message: err.message,
timestamp: new Date().toISOString(),
},
500,
);
});
// Not found handler
app.notFound((c) => {
return c.json(
{
error: "Not found",
path: c.req.path,
},
404,
);
});Monitoring
Logging
typescript
// Structured logging
app.use("*", async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(
JSON.stringify({
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration,
timestamp: new Date().toISOString(),
}),
);
});Analytics
Cloudflare Workers Analytics provides:
- Request count
- Error rate
- CPU time
- Duration percentiles
Access via Cloudflare Dashboard or Workers Analytics API.
Security
Rate Limiting
typescript
// Simple rate limiting
const rateLimiter = new Map<string, number[]>();
async function rateLimit(c: Context, limit: number = 10): Promise<boolean> {
const ip = c.req.header("CF-Connecting-IP") || "unknown";
const now = Date.now();
const windowMs = 60000; // 1 minute
const requests = rateLimiter.get(ip) || [];
const recentRequests = requests.filter((time) => now - time < windowMs);
if (recentRequests.length >= limit) {
return false;
}
recentRequests.push(now);
rateLimiter.set(ip, recentRequests);
return true;
}
// Use in routes
app.post("/api/endpoint", async (c) => {
if (!(await rateLimit(c, 10))) {
return c.json({ error: "Rate limit exceeded" }, 429);
}
// Process request
});Input Validation
typescript
import { z } from "zod";
const schema = z.object({
userId: z.number().positive(),
email: z.string().email(),
role: z.enum(["student", "instructor", "admin"]),
});
app.post("/api/create-user", async (c) => {
const body = await c.req.json();
try {
const validated = schema.parse(body);
// Process validated data
} catch (error) {
return c.json({ error: "Validation failed", details: error.errors }, 400);
}
});Best Practices
- Keep functions small - Edge functions have execution time limits
- Use KV for persistence - Worker memory is ephemeral
- Implement caching - Leverage edge caching for performance
- Handle errors gracefully - Return meaningful error responses
- Monitor performance - Use Cloudflare Analytics
- Secure endpoints - Implement authentication and rate limiting
- Type everything - Use TypeScript for safety
- Test locally - Use
wrangler devbefore deploying
Limitations
- CPU time: 50ms (free), 50ms-30s (paid)
- Memory: 128 MB
- Request size: 100 MB
- Response size: Unlimited
- Execution time: Max 30 seconds (paid plan)
Integration with Backend
The backend can call cloud functions:
typescript
// apps/client-backend/src/gateways/cloudFunctions.gateway.ts
@singleton()
export class CloudFunctionsGateway {
private baseUrl = process.env.CLOUD_FUNCTIONS_URL;
async processFile(file: Buffer): Promise<ProcessResult> {
const formData = new FormData();
formData.append("file", new Blob([file]));
const response = await fetch(`${this.baseUrl}/api/process-file`, {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${this.getServiceToken()}`,
},
});
return response.json();
}
private getServiceToken(): string {
// Generate service-to-service token
return jwt.sign({ service: "backend" }, process.env.JWT_SECRET);
}
}Related Documentation
- Backend Architecture - Backend integration
- Deployment Guide - Full deployment process
- Architecture Overview - System design