Skip to content

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.

Terminal window
pnpm add @cloudwerk/service

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,
})
ParameterTypeDescription
methodsRecord<string, Function>Service methods
namestringOverride service name (default: directory name)
hooksServiceHooksLifecycle hooks
configServiceExtractionConfigExtraction configuration

Returns a ServiceDefinition object that Cloudwerk registers automatically.


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 }
},
},
})

Inside service methods, this provides:

PropertyTypeDescription
this.envEnvEnvironment bindings (D1, KV, R2, secrets)

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>
}
  1. onInit - Once when service initializes
  2. onBefore - Before each method call
  3. Method execution
  4. onAfter - On successful completion
  5. onError - On failure (replaces onAfter)

Called once when the service is first initialized.

hooks: {
onInit: async () => {
console.log('Service initialized')
// Initialize connections, load config, etc.
},
}

Called before every method invocation.

hooks: {
onBefore: async (method, args) => {
console.log(`[${method}] called with`, args)
// Logging, validation, rate limiting, etc.
},
}

Called after successful method completion.

hooks: {
onAfter: async (method, result) => {
console.log(`[${method}] returned`, result)
// Analytics, metrics, caching, etc.
},
}

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)
},
}
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}`)
},
},
})

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
}
}
export default defineService({
methods: {
async send(params) {
// ...
},
},
config: {
extraction: {
workerName: 'email-service',
bindings: ['RESEND_API_KEY', 'DB'],
},
},
})

Services can run in three modes configured in cloudwerk.config.ts.

Services run as direct function calls in the main Worker.

// cloudwerk.config.ts
export 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

Services run as separate Workers using Cloudflare service bindings.

// cloudwerk.config.ts
export default defineConfig({
services: {
mode: 'extracted', // All services as separate Workers
},
})

Characteristics:

  • Isolated resource usage
  • Independent scaling
  • Separate deployment
  • Best for: resource-intensive operations, microservices

Mix local and extracted services.

// cloudwerk.config.ts
export default defineConfig({
services: {
mode: 'hybrid',
email: { mode: 'extracted' },
analytics: { mode: 'extracted' },
cache: { mode: 'local' },
utils: { mode: 'local' },
},
})

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 })
}

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: '...' })

Check if a service exists.

import { hasService } from '@cloudwerk/core/bindings'
if (hasService('email')) {
await services.email.send({ ... })
}

List all available service names.

import { getServiceNames } from '@cloudwerk/core/bindings'
const available = getServiceNames()
// ['email', 'payments', 'cache']

When a service is extracted, Cloudwerk generates a WorkerEntrypoint class.

// Auto-generated: .cloudwerk/extracted/email-service/worker.ts
import { 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)
}
}

For extracted services, bindings are auto-configured:

# wrangler.toml (auto-generated)
[[services]]
binding = "EMAIL_SERVICE"
service = "email-service"
entrypoint = "EmailService"

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 binding

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)
}
}

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)
}
}

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)
}
}

Terminal window
cloudwerk services list
Terminal window
cloudwerk services info <name>

Extract a service to a separate Worker.

Terminal window
cloudwerk services extract <name>

Convert an extracted service back to local mode.

Terminal window
cloudwerk services inline <name>

Deploy an extracted service.

Terminal window
cloudwerk services deploy <name>
Terminal window
cloudwerk services status
Terminal window
cloudwerk services generate-types

interface ServiceDefinition {
methods: Record<string, ServiceMethod>
name?: string
hooks?: ServiceHooks
config?: ServiceConfig
}
type ServiceMethod = (...args: any[]) => Awaitable<any>
interface ServiceConfig {
extraction?: {
workerName?: string
bindings?: string[]
}
}
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>
}
interface CloudwerkConfig {
services?: {
mode: 'local' | 'extracted' | 'hybrid'
[serviceName: string]: { mode: 'local' | 'extracted' }
}
}

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
// Good: Focused, single-domain service
export default defineService({
methods: {
async sendEmail(params) { /* ... */ },
async sendBatch(emails) { /* ... */ },
async getDeliveryStatus(id) { /* ... */ },
},
})
// Avoid: Mixed concerns
export default defineService({
methods: {
async sendEmail(params) { /* ... */ },
async processPayment(params) { /* ... */ }, // Different domain
async generateReport(params) { /* ... */ }, // Different domain
},
})
// Good: Logging/metrics in hooks
export 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`)
},
},
})