Skip to content

Type-Safe Bindings

When using Service Binding upstreams, the service field is a string that must match a binding name in your wrangler.jsonc. A typo means a runtime 502 error. The TBindings generic parameter lets TypeScript catch these mistakes at compile time and gives you IDE autocomplete for valid binding names.

GatewayConfig accepts an optional type parameter TBindings that flows through the entire config tree:

GatewayConfig<TBindings>
└─ routes: RouteConfig<TBindings>[]
└─ pipeline: PipelineConfig<TBindings>
└─ upstream: UpstreamConfig<TBindings>
└─ ServiceBindingUpstream<TBindings>
└─ service: Extract<keyof TBindings, string>

When TBindings is provided, the service field on ServiceBindingUpstream is constrained to Extract<keyof TBindings, string> — only string keys from your bindings type are accepted.

Define an Env interface that matches your wrangler.jsonc bindings, then pass it to createGateway:

import { createGateway, jwtAuth } from "@homegrower-club/stoma";
// Your Worker's Env interface (matches wrangler.jsonc bindings)
interface Env {
AUTH_SERVICE: Fetcher;
USERS_SERVICE: Fetcher;
RATE_LIMIT_KV: KVNamespace;
}
const gateway = createGateway<Env>({
name: "my-api",
routes: [
{
path: "/auth/*",
pipeline: {
upstream: {
type: "service-binding",
service: "AUTH_SERVICE", // Autocompletes to "AUTH_SERVICE" | "USERS_SERVICE" | "RATE_LIMIT_KV"
},
},
},
{
path: "/users/*",
pipeline: {
policies: [jwtAuth({ secret: "..." })],
upstream: {
type: "service-binding",
service: "USERS_SERVICE", // Valid
},
},
},
{
path: "/orders/*",
pipeline: {
upstream: {
type: "service-binding",
// @ts-expect-error -- "ORDERS_SERVICE" does not exist in Env
service: "ORDERS_SERVICE",
},
},
},
],
});

The third route produces a TypeScript error because "ORDERS_SERVICE" is not a key of Env. Without the generic parameter, this would silently compile and fail at runtime.

The default type parameter is Record<string, unknown>, which means Extract<keyof Record<string, unknown>, string> resolves to string. Existing gateway configs that omit the generic continue to accept any string for service — no migration is needed.

// No generic -- service accepts any string (existing behavior)
const gateway = createGateway({
routes: [
{
path: "/api/*",
pipeline: {
upstream: {
type: "service-binding",
service: "ANY_STRING_IS_FINE", // No type error
},
},
},
],
});

Both scope() and mergeConfigs() accept the same TBindings generic parameter, so type safety propagates through route composition and config splitting:

import { createGateway, scope, jwtAuth, cors } from "@homegrower-club/stoma";
import { mergeConfigs } from "@homegrower-club/stoma/config";
interface Env {
AUTH_SERVICE: Fetcher;
PROJECTS_SERVICE: Fetcher;
}
// scope() preserves TBindings
const apiRoutes = scope<Env>({
prefix: "/api/v1",
policies: [jwtAuth({ secret: "..." })],
routes: [
{
path: "/projects/*",
pipeline: {
upstream: {
type: "service-binding",
service: "PROJECTS_SERVICE", // Type-checked against Env
},
},
},
],
});
// mergeConfigs() preserves TBindings
const config = mergeConfigs<Env>(
{ name: "my-api", policies: [cors()] },
{ routes: apiRoutes },
);
const gateway = createGateway<Env>(config);

See Route Scopes for details on scope().