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.
Prerequisites
Section titled “Prerequisites”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.
import { Stratal } from 'stratal'import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })Export the entrypoint class
Section titled “Export the entrypoint class”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:
import { Stratal } from 'stratal'import { AppModule } from './app.module'
export { TaskRpc } from './task/task-rpc'
export default new Stratal({ module: AppModule })Cross-worker bindings
Section titled “Cross-worker bindings”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() }) }}How runInScope works
Section titled “How runInScope works”When you call this.runInScope(callback), Stratal:
- Resolves the
Stratalapplication via the static singleton. - Creates a new request-scoped DI container.
- 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.
Calling from a controller
Section titled “Calling from a controller”Same-worker (loopback RPC via exports)
Section titled “Same-worker (loopback RPC via exports)”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 }) }}Cross-worker (via env binding)
Section titled “Cross-worker (via env binding)”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 }) }}Next steps
Section titled “Next steps”- Dependency Injection to understand how the DI container and scoping works.
- Durable Objects for stateful objects with DI support.
- Workflows for multi-step orchestration with DI support.