Skip to content

Domain Routing

Domain routing lets you match routes based on the request’s hostname rather than just the URL path. You can extract parameters from subdomain patterns, making it straightforward to build multi-tenant applications, regional endpoints, and custom domain logic.

Apply a domain constraint directly on a controller using the domain option in the @Controller() decorator. All routes within the controller will only match when the request hostname fits the pattern.

import { Controller, Route, type RouterContext } from 'stratal/router'
@Controller('/dashboard', { domain: '{tenant}.myapp.com' })
class DashboardController {
@Route()
index(ctx: RouterContext) {
const tenant = ctx.domain('tenant')
return ctx.json({ tenant, message: `Welcome to ${tenant}'s dashboard` })
}
}

Parameters wrapped in curly braces (e.g., {tenant}) are extracted from the hostname and made available through ctx.domain().

For broader control, set the domain constraint at the module level by implementing the RouteConfigurable interface. Every controller registered in the module will inherit the domain rule.

import { Module, type RouteConfigurable, type Router } from 'stratal/module'
@Module({
controllers: [DashboardController],
})
class TenantModule implements RouteConfigurable {
configureRoutes(router: Router) {
router.domain('{tenant}.myapp.com')
}
}

Use ctx.domain() inside any route handler to retrieve a captured hostname segment by name.

const tenant = ctx.domain('tenant')

Domain patterns can contain more than one parameter. Each segment between dots is matched independently.

// Pattern: '{region}.{tenant}.example.com'
// Request: us-east.acme.example.com
const region = ctx.domain('region') // 'us-east'
const tenant = ctx.domain('tenant') // 'acme'

Use router.group() inside configureRoutes to scope a set of controllers under a shared domain, prefix, and middleware.

import { Module, type RouteConfigurable, type Router } from 'stratal/module'
@Module({
controllers: [DashboardController, SettingsController],
})
class TenantModule implements RouteConfigurable {
configureRoutes(router: Router) {
router
.domain('{tenant}.myapp.com')
.group([DashboardController, SettingsController], (r) => {
r.prefix('/api').middleware(TenantMiddleware)
})
}
}

In this example, both DashboardController and SettingsController are reachable only when the request matches {tenant}.myapp.com, all paths are prefixed with /api, and TenantMiddleware runs before every handler.