Services
Cloudwerk provides a service extraction system through @cloudwerk/service. Define reusable services that can run locally (direct calls) or be extracted to separate Workers (RPC via service bindings) - same API, same code, different deployment modes.
Quick Start
Section titled “Quick Start”-
Create a service:
// app/services/email/service.tsimport { defineService } from '@cloudwerk/service'export default defineService({methods: {async send({ to, subject, body }) {const response = await fetch('https://api.resend.com/emails', {method: 'POST',headers: {'Authorization': `Bearer ${this.env.RESEND_API_KEY}`,'Content-Type': 'application/json',},body: JSON.stringify({ to, subject, html: body }),})const data = await response.json()return { success: true, messageId: data.id }},},}) -
Use it from your routes:
// app/api/contact/route.tsimport { services } from '@cloudwerk/core/bindings'import { json } from '@cloudwerk/core'export async function POST(request: Request) {const { email, message } = await request.json()// Works identically whether local or extractedconst result = await services.email.send({subject: 'Contact Form',body: message,})return json({ success: true, messageId: result.messageId })} -
Services are automatically discovered and registered during build.
Convention Structure
Section titled “Convention Structure”Services are defined in the app/services/ directory:
Directoryapp/services/
Directoryemail/
- service.ts # Required: service definition
- utils.ts # Optional: helper utilities
Directorypayments/
- service.ts
Directoryanalytics/
- service.ts
Naming Convention:
- Directory names are kebab-case to camelCase service names
email/becomes service nameemail, bindingEMAIL_SERVICEuser-management/becomes service nameuserManagement, bindingUSER_MANAGEMENT_SERVICE
Defining Services
Section titled “Defining Services”Basic Service
Section titled “Basic Service”// app/services/email/service.tsimport { defineService } from '@cloudwerk/service'
interface SendParams { to: string subject: string body: string}
interface SendResult { success: boolean messageId?: string}
export default defineService({ methods: { async send(params: SendParams): Promise<SendResult> { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: params.to, subject: params.subject, html: params.body, }), })
if (!response.ok) { throw new Error(`Email failed: ${response.statusText}`) }
const data = await response.json() return { success: true, messageId: data.id } },
async sendBatch(emails: SendParams[]): Promise<{ sent: number }> { const results = await Promise.all( emails.map(e => this.send(e)) ) return { sent: results.filter(r => r.success).length } }, },})With Lifecycle Hooks
Section titled “With Lifecycle Hooks”export default defineService({ methods: { async processPayment(orderId: string, amount: number) { // Process payment logic return { success: true, transactionId: 'txn_123' } }, },
hooks: { onInit: async () => { console.log('Payment service initialized') },
onBefore: async (method, args) => { console.log(`[payments] ${method} called with`, args) // Logging, validation, rate limiting, etc. },
onAfter: async (method, result) => { console.log(`[payments] ${method} returned`, result) // Analytics, metrics, etc. },
onError: async (method, error) => { console.error(`[payments] ${method} failed:`, error.message) // Error tracking, alerting, etc. await reportError('payments', method, error) }, },})With Extraction Config
Section titled “With Extraction Config”Configure for extraction to a separate Worker:
export default defineService({ methods: { async send(params) { // ... }, },
config: { extraction: { workerName: 'email-service', // Name of extracted Worker bindings: ['RESEND_API_KEY', 'DB'], // Required bindings }, },})Service Modes
Section titled “Service Modes”Services can run in two modes with identical APIs:
Services run as direct function calls in the main Worker:
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'local', // All services run locally },})Advantages:
- No network latency
- No serialization overhead
- Full access to Worker context
Use for:
- Simple utility services
- Low-latency operations
- Services with minimal resource usage
Services run as separate Workers using Cloudflare service bindings:
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'extracted', // All services run as separate Workers },})Advantages:
- Isolated resource usage
- Independent scaling
- Separate deployment
- Better resource management
Use for:
- Resource-intensive operations
- Services that need independent scaling
- Microservices architecture
Mix local and extracted services:
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'hybrid', email: { mode: 'extracted' }, // Runs as separate Worker analytics: { mode: 'extracted' }, // Runs as separate Worker cache: { mode: 'local' }, // Runs locally utils: { mode: 'local' }, // Runs locally },})Best of both worlds:
- Extract expensive/slow services
- Keep fast utilities local
Using Services
Section titled “Using Services”Typed Service Access
Section titled “Typed Service Access”import { services } from '@cloudwerk/core/bindings'
export async function POST(request: Request) { // Type-safe method calls const result = await services.email.send({ subject: 'Hello', body: '<h1>Welcome!</h1>', })
const batchResult = await services.email.sendBatch([ ])
return json({ sent: batchResult.sent })}Check Service Availability
Section titled “Check Service Availability”import { services, hasService, getServiceNames } from '@cloudwerk/core/bindings'
export async function GET() { // Check if service exists if (hasService('email')) { await services.email.send({ ... }) }
// List available services const available = getServiceNames() return json({ services: available })}Type-Safe Service Retrieval
Section titled “Type-Safe Service Retrieval”import { getService } from '@cloudwerk/core/bindings'
interface EmailService { send(params: { to: string; subject: string; body: string }): Promise<{ success: boolean }>}
export async function POST(request: Request) { const email = getService<EmailService>('email') const result = await email.send({ subject: 'Hello', body: 'Welcome!', }) return json(result)}RPC Communication
Section titled “RPC Communication”How It Works
Section titled “How It Works”When a service is extracted, Cloudwerk generates a WorkerEntrypoint class:
// Auto-generated: .cloudwerk/extracted/email-service/worker.tsimport { WorkerEntrypoint } from 'cloudflare:workers'import service from '../../../app/services/email/service'
export class EmailService extends WorkerEntrypoint<Env> { async send(...args: unknown[]): Promise<unknown> { // Runs hooks and calls the actual method return service.methods.send.apply({ env: this.env }, args) }}Transparent Routing
Section titled “Transparent Routing”The services proxy automatically routes calls:
// Your codeawait services.email.send({ to: '...' })
// Local mode: Direct function call// Extracted mode: RPC via service bindingService Bindings
Section titled “Service Bindings”For extracted services, bindings are auto-configured in wrangler.toml:
# Auto-generated[[services]]binding = "EMAIL_SERVICE"service = "email-service"entrypoint = "EmailService"Lifecycle Hooks
Section titled “Lifecycle Hooks”Hook Execution Order
Section titled “Hook Execution Order”- onInit - Once when service initializes
- onBefore - Before each method call
- Method execution
- onAfter - On success
- onError - On failure (replaces onAfter)
Hook Signatures
Section titled “Hook Signatures”interface ServiceHooks { onInit?: () => Awaitable<void> onBefore?: (method: string, args: unknown[]) => Awaitable<void> onAfter?: (method: string, result: unknown) => Awaitable<void> onError?: (method: string, error: Error) => Awaitable<void>}Logging Hook Example
Section titled “Logging Hook Example”export default defineService({ methods: { async processOrder(orderId: string) { // Business logic }, },
hooks: { onBefore: async (method, args) => { console.log(JSON.stringify({ service: 'orders', method, args, timestamp: Date.now(), })) },
onAfter: async (method, result) => { console.log(JSON.stringify({ service: 'orders', method, success: true, timestamp: Date.now(), })) },
onError: async (method, error) => { // Report to error tracking await fetch('https://errors.example.com/report', { method: 'POST', body: JSON.stringify({ service: 'orders', method, error: error.message, stack: error.stack, }), }) }, },})Common Patterns
Section titled “Common Patterns”Email Service
Section titled “Email Service”// app/services/email/service.tsimport { defineService } from '@cloudwerk/service'
export default defineService({ methods: { async send({ to, subject, html, text }) { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to, subject, html, text, }), })
if (!response.ok) { throw new Error(`Email API error: ${response.status}`) }
return response.json() },
async sendTemplate(templateId: string, to: string, data: Record<string, unknown>) { const template = await this.getTemplate(templateId) const html = this.renderTemplate(template, data) return this.send({ to, subject: template.subject, html }) }, },
config: { extraction: { workerName: 'email-service', bindings: ['RESEND_API_KEY'], }, },})Payment Service
Section titled “Payment Service”// app/services/payments/service.tsimport { defineService } from '@cloudwerk/service'
export default defineService({ methods: { async createCheckout({ customerId, items, successUrl, cancelUrl }) { const response = await fetch('https://api.stripe.com/v1/checkout/sessions', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ customer: customerId, mode: 'payment', success_url: successUrl, cancel_url: cancelUrl, 'line_items[0][price]': items[0].priceId, 'line_items[0][quantity]': items[0].quantity.toString(), }), })
return response.json() },
async createCustomer({ email, name }) { const response = await fetch('https://api.stripe.com/v1/customers', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ email, name }), })
return response.json() },
async getPaymentIntent(intentId: string) { const response = await fetch( `https://api.stripe.com/v1/payment_intents/${intentId}`, { headers: { 'Authorization': `Bearer ${this.env.STRIPE_SECRET_KEY}`, }, } )
return response.json() }, },
hooks: { onBefore: async (method, args) => { // Audit logging for payments console.log(`[payments] ${method} called`) }, },
config: { extraction: { workerName: 'payment-service', bindings: ['STRIPE_SECRET_KEY', 'DB'], }, },})Cache Service
Section titled “Cache Service”// app/services/cache/service.tsimport { defineService } from '@cloudwerk/service'
export default defineService({ methods: { async get<T>(key: string): Promise<T | null> { const value = await this.env.KV.get(key, 'json') return value as T | null },
async set(key: string, value: unknown, ttl?: number) { await this.env.KV.put(key, JSON.stringify(value), { expirationTtl: ttl, }) },
async delete(key: string) { await this.env.KV.delete(key) },
async getOrSet<T>(key: string, fn: () => Promise<T>, ttl?: number): Promise<T> { const cached = await this.get<T>(key) if (cached !== null) return cached
const value = await fn() await this.set(key, value, ttl) return value }, },})CLI Commands
Section titled “CLI Commands”Service Management
Section titled “Service Management”# List all servicescloudwerk services list
# Show service detailscloudwerk services info email
# Extract a service to separate Workercloudwerk services extract email
# Convert back to local modecloudwerk services inline email
# Deploy extracted servicecloudwerk services deploy email
# Show all service statuscloudwerk services statusType Generation
Section titled “Type Generation”# Generate types for all servicescloudwerk services generate-typesError Handling
Section titled “Error Handling”import { ServiceNotFoundError, ServiceMethodError,} from '@cloudwerk/service'
try { await services.email.send({ to: '...' })} catch (error) { if (error instanceof ServiceNotFoundError) { console.error('Service not available:', error.serviceName) } if (error instanceof ServiceMethodError) { console.error('Method failed:', error.methodName, error.message) }}Best Practices
Section titled “Best Practices”When to Extract
Section titled “When to Extract”Extract to separate Worker when:
- Service makes many external API calls
- Service is CPU or memory intensive
- Service needs independent scaling
- Service should be deployable separately
Keep local when:
- Service is a thin wrapper or utility
- Low latency is critical
- Service has minimal resource usage
- Service is tightly coupled to main app
Configuration Reference
Section titled “Configuration Reference”ServiceConfig
Section titled “ServiceConfig”interface ServiceConfig { methods: Record<string, Function> // Required name?: string // Override service name hooks?: ServiceHooks // Lifecycle hooks config?: { extraction?: { workerName?: string // Default: {name}-service bindings?: string[] // Required bindings } }}App Config
Section titled “App Config”// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'local' | 'extracted' | 'hybrid', // Per-service overrides (hybrid mode) email: { mode: 'extracted' }, cache: { mode: 'local' }, },})Next Steps
Section titled “Next Steps”- Services API Reference - Complete API documentation
- Durable Objects Guide - Stateful edge computing
- Queues Guide - Background job processing