Route Scopes
The scope() function groups routes under a shared path prefix, prepends shared
policies, and merges metadata — eliminating repetition when multiple routes
share the same base path or middleware stack. It returns a flat RouteConfig[]
that spreads directly into GatewayConfig.routes.
import { scope } from "@homegrower-club/stoma";ScopeConfig
Section titled “ScopeConfig”| Field | Type | Description |
|---|---|---|
prefix | string | Path prefix prepended to every child route (e.g. "/api/v1"). Normalized automatically — leading / is added if missing, trailing / is stripped. |
policies | Policy[] | Policies prepended to every child route’s pipeline policies. Optional. |
routes | RouteConfig[] | Child routes to scope. |
metadata | Record<string, unknown> | Metadata merged into every child route. Child values win on conflict. Optional. |
scope() accepts a TBindings generic that propagates to child routes:
scope<MyEnv>({ prefix: "/api", routes: [...] })Basic example
Section titled “Basic example”Group versioned API routes under a shared prefix with JWT authentication:
import { createGateway, scope, jwtAuth, rateLimit } from "@homegrower-club/stoma";
const gateway = createGateway({ routes: [ ...scope({ prefix: "/api/v1", policies: [ jwtAuth({ jwksUrl: "https://auth.example.com/.well-known/jwks.json" }), rateLimit({ max: 200, windowSeconds: 60 }), ], routes: [ { path: "/users", pipeline: { upstream: { type: "url", target: "https://users.internal" }, }, }, { path: "/orders", pipeline: { upstream: { type: "url", target: "https://orders.internal" }, }, }, ], }), ],});This produces two routes: /api/v1/users and /api/v1/orders, both with
jwtAuth and rateLimit in their pipelines.
Policy layering
Section titled “Policy layering”Scope policies are prepended to each child route’s existing policies. They are not deduplicated by name — both the scope policy and any route-level policy with the same name will appear in the final pipeline array.
const routes = scope({ prefix: "/api", policies: [cors(), rateLimit({ max: 100 })], routes: [ { path: "/users", pipeline: { policies: [requestValidation({ validate: myValidator })], upstream: { type: "url", target: "https://users.internal" }, }, }, ],});
// routes[0].pipeline.policies is:// [cors(), rateLimit({ max: 100 }), requestValidation({ ... })]The resulting array order is: scope policies first, then route policies. When
this array reaches createGateway(), the gateway merges it with
global policies using
the standard deduplicate-by-name, sort-by-priority algorithm.
Metadata merging
Section titled “Metadata merging”Scope metadata is shallow-merged into each child route. When both the scope and a child route define the same key, the child value wins:
const routes = scope({ prefix: "/api", metadata: { team: "platform", version: "v1" }, routes: [ { path: "/users", metadata: { team: "users", owner: "alice" }, pipeline: { upstream: { type: "url", target: "https://users.internal" }, }, }, ],});
// routes[0].metadata is:// { team: "users", version: "v1", owner: "alice" }Nesting
Section titled “Nesting”Scopes nest naturally — pass the output of an inner scope() as the routes
of an outer scope(). Prefixes accumulate, policies accumulate (outer prepends
before inner), and metadata accumulates (innermost wins):
const v1Routes = scope({ prefix: "/v1", policies: [jwtAuth({ secret: env.JWT_SECRET })], metadata: { version: "v1" }, routes: [ { path: "/users", pipeline: { upstream: { type: "url", target: "https://users.internal" }, }, }, { path: "/orders", pipeline: { upstream: { type: "url", target: "https://orders.internal" }, }, }, ],});
const apiRoutes = scope({ prefix: "/api", policies: [cors(), requestLog()], metadata: { team: "platform" }, routes: v1Routes,});
// apiRoutes[0].path === "/api/v1/users"// apiRoutes[0].policies === [cors(), requestLog(), jwtAuth(...)]// apiRoutes[0].metadata === { team: "platform", version: "v1" }Three or more levels work the same way — each scope() call produces a flat
RouteConfig[] that the next level consumes.
Composition with createGateway
Section titled “Composition with createGateway”Scoped routes spread alongside non-scoped routes in the routes array:
// Route scopes: group routes under shared path prefixes and policies
// using scope(). Eliminates repetition when multiple routes share
// the same base path or middleware stack.
// Demo API: https://stoma.opensource.homegrower.club/demo-api
import { createGateway, scope, health, jwtAuth, cors } from "@homegrower-club/stoma";
// Scoped routes share a prefix and JWT auth policy
const apiRoutes = scope({
prefix: "/api/v1",
policies: [jwtAuth({ secret: "my-jwt-secret" })],
routes: [
{
path: "/users/*",
pipeline: {
upstream: { type: "url", target: "https://stoma.opensource.homegrower.club/demo-api" },
},
},
{
path: "/projects/*",
pipeline: {
upstream: { type: "url", target: "https://stoma.opensource.homegrower.club/demo-api" },
},
},
],
});
const gateway = createGateway({
name: "my-api",
policies: [cors()],
routes: [
// Health check lives outside any scope
health({ path: "/health" }),
// Scoped routes at /api/v1/users/* and /api/v1/projects/*
...apiRoutes,
],
});
export default gateway;
Open in Editor
The health check lives at /health outside any scope. The API routes live at
/api/v1/users/* and /api/v1/projects/* with JWT auth from the scope and
CORS from the global policies.