Skip to content

Service Bindings

Stratal provides StratalWorkerEntrypoint, a base class that extends Cloudflare’s native WorkerEntrypoint with full access to your application’s DI container. This lets you expose RPC methods that other workers (or the same worker) can call via Service Bindings, while sharing the same services and providers as the rest of your application.

Your worker entry file must export a Stratal instance as the default export. The worker base classes use the static singleton to resolve the application and its DI container at runtime.

src/index.ts
import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })

Service Binding entrypoints must be named exports from your worker entry file. No special wrangler configuration is needed when the entrypoint lives in the same worker:

src/index.ts
import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export { TaskRpc } from './task/task-rpc'
export default new Stratal({ module: AppModule })

To call an entrypoint from a different worker, add a service binding in the caller’s wrangler.jsonc:

{
"services": [
{
"binding": "TASK_SERVICE",
"service": "task-worker",
"entrypoint": "TaskRpc"
}
]
}

The entrypoint field specifies which named export to bind to. The caller can then invoke methods on env.TASK_SERVICE as typed RPC calls.

Extend StratalWorkerEntrypoint and use this.runInScope() to access the DI container inside your RPC methods:

import { StratalWorkerEntrypoint } from 'stratal/workers'
import type { Task } from './task.service'
import { TaskService } from './task.service'
export class TaskRpc extends StratalWorkerEntrypoint {
async getTask(id: string): Promise<Task | undefined> {
return this.runInScope(async (container) => {
const taskService = container.resolve(TaskService)
return taskService.findById(id)
})
}
async getTasksByUser(userId: string): Promise<Task[]> {
return this.runInScope(async (container) => {
const taskService = container.resolve(TaskService)
return taskService.findByUserId(userId)
})
}
async getTaskCount(): Promise<number> {
return this.runInScope(async (container) => {
const taskService = container.resolve(TaskService)
return taskService.count()
})
}
}

When you call this.runInScope(callback), Stratal:

  1. Resolves the Stratal application via the static singleton.
  2. Creates a new request-scoped DI container.
  3. Invokes your callback with the container, so you can resolve any registered service.

Each RPC call gets its own isolated scope, just like an HTTP request would.

When the entrypoint lives in the same worker, use the exports helper from cloudflare:workers. No wrangler service binding is needed:

import { exports } from 'cloudflare:workers'
import type { StratalEnv } from 'stratal'
import { DI_TOKENS, inject } from 'stratal/di'
import { Controller, type IController, Route, type RouterContext, uuidParamSchema } from 'stratal/router'
import { z } from 'stratal/validation'
@Controller('/api/tasks')
export class TaskController implements IController {
constructor(
@inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv
) {}
@Route({
response: z.object({ task: taskSchema.nullable() }),
summary: 'Get a task by ID (via RPC loopback)',
params: uuidParamSchema
})
async show(ctx: RouterContext) {
const id = ctx.param('id')
// Look up the task via the loopback RPC export
const task = await exports.TaskRpc.getTask(id)
return ctx.json({ task: task ?? null })
}
}

When calling an entrypoint on a different worker, inject the Cloudflare environment and use the service binding configured in wrangler.jsonc:

@Controller('/api/tasks')
export class TaskController implements IController {
constructor(
@inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv
) {}
@Route({
response: z.object({ task: taskSchema.nullable() }),
summary: 'Get a task by ID (via cross-worker binding)',
params: uuidParamSchema
})
async show(ctx: RouterContext) {
const id = ctx.param('id')
const task = await this.env.TASK_SERVICE.getTask(id)
return ctx.json({ task: task ?? null })
}
}