Migration Guide
Migration Guide
Migrating from raw Cloudflare APIs to workkit. Each section shows a before/after for one package.
Environment Validation
Before (raw)
export default { async fetch(request: Request, env: any) { // No validation -- runtime errors deep in handlers const db = env.DB // might be undefined const key = env.API_KEY // might be empty string },}After (workkit)
import { parseEnvSync } from '@workkit/env'import { z } from 'zod'
const schema = { DB: z.custom<D1Database>((v) => v != null), API_KEY: z.string().min(1),}
export default { async fetch(request: Request, rawEnv: Record<string, unknown>) { const env = parseEnvSync(rawEnv, schema) // Fails fast at startup with ALL issues listed // env is fully typed: { DB: D1Database, API_KEY: string } },}D1 Queries
Before (raw)
const stmt = env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(id)const result = await stmt.first()// result is Record<string, unknown> | null// No column transforms, no error classificationAfter (workkit)
import { d1 } from '@workkit/d1'
const db = d1(env.DB, { transformColumns: 'camelCase' })
const user = await db.first<User>('SELECT * FROM users WHERE id = ?', [id])// user is User | null, with camelCase columns// D1 errors are classified: D1QueryError, D1ConstraintError, etc.
// Or use the fluent builder:const user = await db.select<User>('users').where('id = ?', [id]).first()KV Operations
Before (raw)
const raw = await env.MY_KV.get('user:alice', 'json')// raw is unknown// No type safety, manual prefix handling, no batch operationsAfter (workkit)
import { kv } from '@workkit/kv'
const users = kv<User>(env.MY_KV, { prefix: 'user:', defaultTtl: 3600 })
const user = await users.get('alice') // User | null, type-safeawait users.put('alice', { name: 'Alice', role: 'admin' }) // type-checked
// Batch operationsconst all = await users.getMany(['alice', 'bob', 'charlie'])
// Async iteration over keysfor await (const entry of users.list()) { /* ... */ }Queue Producing
Before (raw)
await env.MY_QUEUE.send({ type: 'created', userId: '123' })// No type safety on message shapeAfter (workkit)
import { queue } from '@workkit/queue'
interface UserEvent { type: 'created' | 'updated' | 'deleted' userId: string}
const events = queue<UserEvent>(env.MY_QUEUE)await events.send({ type: 'created', userId: '123' }) // type-checked// events.send({ type: 'invalid' }) -- compile errorQueue Consuming
Before (raw)
export default { async queue(batch: MessageBatch, env: Env) { for (const message of batch.messages) { try { await processMessage(message.body) message.ack() } catch { message.retry() } } },}After (workkit)
import { createConsumer, RetryAction } from '@workkit/queue'
const handler = createConsumer<UserEvent>({ async process(message) { await processMessage(message.body) // void = auto ack }, maxRetries: 3, deadLetterQueue: env.DLQ, concurrency: 5, onError: (error, message) => console.error(error),})
export default { async queue(batch, env) { await handler(batch, env) },}Cron Handling
Before (raw)
export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { switch (event.cron) { case '*/15 * * * *': await syncData(env) break case '0 0 * * *': await cleanup(env) break } },}After (workkit)
import { createCronHandler, withTimeout, withRetry } from '@workkit/cron'
const handler = createCronHandler<Env>({ tasks: { 'sync-data': { schedule: '*/15 * * * *', handler: async (event, env) => await syncData(env), }, 'cleanup': { schedule: '0 0 * * *', handler: async (event, env) => await cleanup(env), }, }, middleware: [ withTimeout(30000), withRetry(3), ],})
export default { scheduled: handler,}JWT Authentication
Before (raw)
// Manual JWT verification with WebCrypto -- 50+ lines of base64/HMAC codeAfter (workkit)
import { signJWT, verifyJWT, extractBearerToken } from '@workkit/auth'
const token = await signJWT( { userId: 'user-123' }, { secret: env.JWT_SECRET, expiresIn: '24h' },)
const payload = await verifyJWT(token, { secret: env.JWT_SECRET })Password Hashing
Before (raw)
// Manual PBKDF2 with WebCrypto, salt generation, hex encoding -- 40+ linesAfter (workkit)
import { hashPassword, verifyPassword } from '@workkit/auth'
const hashed = await hashPassword('secret')const valid = await verifyPassword('secret', hashed)Rate Limiting
Before (raw)
// Manual KV-based counter with window calculation, TTL managementAfter (workkit)
import { fixedWindow, rateLimitResponse } from '@workkit/ratelimit'
const limiter = fixedWindow({ namespace: env.RATE_LIMIT_KV, limit: 100, window: '1m',})
const result = await limiter.check(ip)if (!result.allowed) return rateLimitResponse(result)Error Handling
Before (raw)
try { // ...} catch (error) { // What status code? Is it retryable? What's the error shape? return new Response(JSON.stringify({ error: 'Something went wrong' }), { status: 500, headers: { 'Content-Type': 'application/json' }, })}After (workkit)
import { NotFoundError, errorToResponse, isRetryable } from '@workkit/errors'
try { const user = await db.first<User>('SELECT ...', [id]) if (!user) throw new NotFoundError('User', id) return Response.json(user)} catch (error) { // Auto maps to correct HTTP status, includes error code, strips internals return errorToResponse(error)}Durable Objects
Before (raw)
export class Counter implements DurableObject { async fetch(request: Request): Promise<Response> { const count = await this.state.storage.get('count') ?? 0 // count is unknown -- needs manual casting await this.state.storage.put('count', count + 1) return Response.json({ count: count + 1 }) }}After (workkit)
import { typedStorage } from '@workkit/do'
interface Schema { count: number; lastUpdated: string }
export class Counter implements DurableObject { private storage = typedStorage<Schema>(this.state.storage)
async fetch(request: Request): Promise<Response> { const count = await this.storage.get('count') ?? 0 // count is number | undefined -- type-safe await this.storage.put('count', count + 1) // type-checked await this.storage.put('lastUpdated', new Date().toISOString()) return Response.json({ count: count + 1 }) }}Testing
Before (raw)
// Miniflare, or manual mock objects with partial interfacesconst mockKV = { get: async () => null, put: async () => {}, // ... many more methods to stub}After (workkit)
import { createTestEnv, createRequest, createExecutionContext } from '@workkit/testing'
const env = createTestEnv({ kv: ['CACHE'] as const, d1: ['DB'] as const, vars: { API_KEY: 'test' },})
const req = createRequest('/api/users', { method: 'POST', body: { name: 'Alice' } })const ctx = createExecutionContext()const res = await handler.fetch(req, env, ctx)