Services API
The @cloudwerk/service package provides a service extraction system for building reusable services that can run locally or be extracted to separate Workers with RPC communication.
Installation
Section titled “Installation”pnpm add @cloudwerk/servicedefineService()
Section titled “defineService()”Creates a service definition with methods and optional lifecycle hooks.
import { defineService } from '@cloudwerk/service'
export default defineService({ methods: Record<string, ServiceMethod>, name?: string, hooks?: ServiceHooks, config?: ServiceExtractionConfig,})Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
methods | Record<string, Function> | Service methods |
name | string | Override service name (default: directory name) |
hooks | ServiceHooks | Lifecycle hooks |
config | ServiceExtractionConfig | Extraction configuration |
Returns
Section titled “Returns”Returns a ServiceDefinition object that Cloudwerk registers automatically.
Service Methods
Section titled “Service Methods”Methods are async functions with access to this.env for bindings.
export default defineService({ methods: { async send(params: SendParams): Promise<SendResult> { // Access bindings via this.env 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(params), })
return response.json() },
// Methods can call other methods 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 } }, },})Method Context (this)
Section titled “Method Context (this)”Inside service methods, this provides:
| Property | Type | Description |
|---|---|---|
this.env | Env | Environment bindings (D1, KV, R2, secrets) |
ServiceHooks
Section titled “ServiceHooks”Lifecycle hooks for cross-cutting concerns.
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>}Hook Execution Order
Section titled “Hook Execution Order”- onInit - Once when service initializes
- onBefore - Before each method call
- Method execution
- onAfter - On successful completion
- onError - On failure (replaces onAfter)
onInit()
Section titled “onInit()”Called once when the service is first initialized.
hooks: { onInit: async () => { console.log('Service initialized') // Initialize connections, load config, etc. },}onBefore()
Section titled “onBefore()”Called before every method invocation.
hooks: { onBefore: async (method, args) => { console.log(`[${method}] called with`, args) // Logging, validation, rate limiting, etc. },}onAfter()
Section titled “onAfter()”Called after successful method completion.
hooks: { onAfter: async (method, result) => { console.log(`[${method}] returned`, result) // Analytics, metrics, caching, etc. },}onError()
Section titled “onError()”Called when a method throws an error.
hooks: { onError: async (method, error) => { console.error(`[${method}] failed:`, error.message) // Error tracking, alerting, etc. await reportError('service-name', method, error) },}Complete Hooks Example
Section titled “Complete Hooks Example”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 ready') },
onBefore: async (method, args) => { console.log(JSON.stringify({ service: 'payments', method, args, timestamp: Date.now(), })) },
onAfter: async (method, result) => { await trackMetric('payment_processed', 1) },
onError: async (method, error) => { await alertOps(`Payment ${method} failed: ${error.message}`) }, },})Extraction Configuration
Section titled “Extraction Configuration”Configure how the service runs when extracted to a separate Worker.
interface ServiceExtractionConfig { extraction?: { workerName?: string // Name of extracted Worker (default: {name}-service) bindings?: string[] // Required bindings to forward }}Example
Section titled “Example”export default defineService({ methods: { async send(params) { // ... }, },
config: { extraction: { workerName: 'email-service', bindings: ['RESEND_API_KEY', 'DB'], }, },})Service Modes
Section titled “Service Modes”Services can run in three modes configured in cloudwerk.config.ts.
Local Mode
Section titled “Local Mode”Services run as direct function calls in the main Worker.
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'local', // All services run locally },})Characteristics:
- No network latency
- No serialization overhead
- Full access to Worker context
- Best for: utilities, low-latency operations
Extracted Mode
Section titled “Extracted Mode”Services run as separate Workers using Cloudflare service bindings.
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'extracted', // All services as separate Workers },})Characteristics:
- Isolated resource usage
- Independent scaling
- Separate deployment
- Best for: resource-intensive operations, microservices
Hybrid Mode
Section titled “Hybrid Mode”Mix local and extracted services.
// cloudwerk.config.tsexport default defineConfig({ services: { mode: 'hybrid', email: { mode: 'extracted' }, analytics: { mode: 'extracted' }, cache: { mode: 'local' }, utils: { mode: 'local' }, },})Using Services
Section titled “Using Services”services Proxy
Section titled “services Proxy”Import and use services with type safety.
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>', })
return json({ success: true, messageId: result.messageId })}getService()
Section titled “getService()”Get a typed service by name.
import { getService } from '@cloudwerk/core/bindings'
interface EmailService { send(params: { to: string; subject: string; body: string }): Promise<{ success: boolean }>}
const email = getService<EmailService>('email')const result = await email.send({ to: '...', subject: '...', body: '...' })hasService()
Section titled “hasService()”Check if a service exists.
import { hasService } from '@cloudwerk/core/bindings'
if (hasService('email')) { await services.email.send({ ... })}getServiceNames()
Section titled “getServiceNames()”List all available service names.
import { getServiceNames } from '@cloudwerk/core/bindings'
const available = getServiceNames()// ['email', 'payments', 'cache']RPC Communication
Section titled “RPC Communication”When a service is extracted, Cloudwerk generates a WorkerEntrypoint class.
Generated Worker
Section titled “Generated Worker”// 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> { return service.methods.send.apply({ env: this.env }, args) }}Service Binding
Section titled “Service Binding”For extracted services, bindings are auto-configured:
# wrangler.toml (auto-generated)[[services]]binding = "EMAIL_SERVICE"service = "email-service"entrypoint = "EmailService"Transparent Routing
Section titled “Transparent Routing”The services proxy routes calls automatically:
// Your code (identical for both modes)await services.email.send({ to: '...' })
// Local mode: Direct function call// Extracted mode: RPC via service bindingError Handling
Section titled “Error Handling”ServiceNotFoundError
Section titled “ServiceNotFoundError”Thrown when accessing a non-existent service.
import { ServiceNotFoundError } from '@cloudwerk/service'
try { await services.nonexistent.method()} catch (error) { if (error instanceof ServiceNotFoundError) { console.error('Service not found:', error.serviceName) }}ServiceMethodError
Section titled “ServiceMethodError”Thrown when a service method fails.
import { ServiceMethodError } from '@cloudwerk/service'
try { await services.email.send({ to: '...' })} catch (error) { if (error instanceof ServiceMethodError) { console.error('Method failed:', error.serviceName, error.methodName) console.error('Original error:', error.cause) }}ServiceConnectionError
Section titled “ServiceConnectionError”Thrown when extracted service is unreachable.
import { ServiceConnectionError } from '@cloudwerk/service'
try { await services.email.send({ to: '...' })} catch (error) { if (error instanceof ServiceConnectionError) { console.error('Cannot reach service:', error.serviceName) }}CLI Commands
Section titled “CLI Commands”List Services
Section titled “List Services”cloudwerk services listShow Service Details
Section titled “Show Service Details”cloudwerk services info <name>Extract Service
Section titled “Extract Service”Extract a service to a separate Worker.
cloudwerk services extract <name>Inline Service
Section titled “Inline Service”Convert an extracted service back to local mode.
cloudwerk services inline <name>Deploy Service
Section titled “Deploy Service”Deploy an extracted service.
cloudwerk services deploy <name>Show All Status
Section titled “Show All Status”cloudwerk services statusGenerate Types
Section titled “Generate Types”cloudwerk services generate-typesType Definitions
Section titled “Type Definitions”ServiceDefinition
Section titled “ServiceDefinition”interface ServiceDefinition { methods: Record<string, ServiceMethod> name?: string hooks?: ServiceHooks config?: ServiceConfig}
type ServiceMethod = (...args: any[]) => Awaitable<any>ServiceConfig
Section titled “ServiceConfig”interface ServiceConfig { extraction?: { workerName?: string bindings?: string[] }}ServiceHooks
Section titled “ServiceHooks”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>}App Config
Section titled “App Config”interface CloudwerkConfig { services?: { mode: 'local' | 'extracted' | 'hybrid' [serviceName: string]: { mode: 'local' | 'extracted' } }}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
Service Design
Section titled “Service Design”// Good: Focused, single-domain serviceexport default defineService({ methods: { async sendEmail(params) { /* ... */ }, async sendBatch(emails) { /* ... */ }, async getDeliveryStatus(id) { /* ... */ }, },})
// Avoid: Mixed concernsexport default defineService({ methods: { async sendEmail(params) { /* ... */ }, async processPayment(params) { /* ... */ }, // Different domain async generateReport(params) { /* ... */ }, // Different domain },})Use Hooks for Cross-Cutting Concerns
Section titled “Use Hooks for Cross-Cutting Concerns”// Good: Logging/metrics in hooksexport default defineService({ methods: { async processPayment(orderId, amount) { // Pure business logic only return await stripe.charges.create({ amount }) }, }, hooks: { onBefore: async (method, args) => { await trackMetric(`${method}_started`) }, onAfter: async (method, result) => { await trackMetric(`${method}_completed`) }, },})Next Steps
Section titled “Next Steps”- Services Guide - Patterns and best practices
- Durable Objects API - Stateful services
- Queues API - Background job processing