Routing
MiiaJS uses a trie-based router with a static route fast path. Routes are defined via decorators on controllers, and the router compiles them into an optimized matching structure before handling the first request.
How matching works
When a request arrives, the router resolves a handler in this order:
- Static lookup — O(1) Map check for routes without parameters (e.g.
/users,/health) - Trie traversal — walks the trie for routes with
:paramor*wildcard segments - HEAD fallback — if method is HEAD and no match, retries with GET automatically
Priority
When multiple routes could match a URL, the router picks the most specific one:
static segment > :param > * wildcard
Given these routes:
@Get('/users/me') // static
@Get('/users/:id') // param
@Get('/users/*') // wildcard
A request to /users/me matches the static route. /users/42 matches the param route. /users/42/posts/1 matches the wildcard.
Path parameters
Dynamic segments prefixed with : capture a single path segment:
@Controller('/users')
class UserController {
@Get('/:id')
findOne(ctx: RequestContext) {
return { id: ctx.params.id }
}
}
Multiple parameters work across nested segments:
@Get('/:userId/posts/:postId')
getPost(ctx: RequestContext) {
const { userId, postId } = ctx.params
return { userId, postId }
}
GET /users/42/posts/7 → { userId: '42', postId: '7' }
Parameters are always strings. Parse them in your handler if you need numbers.
Wildcards
A * segment captures the entire remaining path:
app.addRoute('GET', '/files/*', (ctx) => {
// GET /files/images/logo.png → ctx.params['*'] = 'images/logo.png'
return { path: ctx.params['*'] }
})
Wildcards are useful for catch-all routes, static file serving, and SPA fallbacks. The captured value is available as ctx.params['*'].
Trailing slashes
MiiaJS normalizes trailing slashes — /users and /users/ match the same route. You don't need to register both.
@Get('/users')
list() { return [] }
// All of these match:
// GET /users
// GET /users/
HEAD requests
HEAD requests automatically fall back to the matching GET route if no explicit HEAD route is defined. The framework returns headers only, without a body — matching HTTP spec behavior.
@Get('/health')
check() {
return { status: 'ok' }
}
// GET /health → 200 { "status": "ok" }
// HEAD /health → 200 (headers only, no body)
Manual routes
Use app.addRoute() to register routes without decorators:
const app = new Miia()
.addRoute('GET', '/health', () => ({ ok: true }))
.addRoute('GET', '/users/:id', (ctx) => {
return { id: ctx.params.id }
})
.addRoute('POST', '/upload', uploadHandler, [authMiddleware])
.register(AppModule)
app.addRoute(
method: HttpMethod,
path: string,
handler: (ctx: RequestContext) => any,
middlewares?: Middleware[],
): this
Manual routes are standalone — global middleware from app.use() is not applied. Only the middlewares passed directly to addRoute() are used.
Route compilation
Before handling the first request, MiiaJS compiles all routes into optimized pipelines:
Decorator routes
The full middleware stack is assembled in this order:
Controller @UseGuard() guards
→ Controller @Use() middlewares
→ Method @UseGuard() guards
→ Method @Use() middlewares
→ Validation decorators
→ Route handler
Global middleware from app.use() is prepended to this stack. The entire chain is pre-compiled into a single compose() function.
Performance
- Static routes are stored in a
Mapfor O(1) exact matching — no trie traversal needed - URL parsing uses
indexOf()instead of regex for fast pathname/query extraction - Query params are parsed lazily — only when
ctx.queryis accessed - Sync fast path — routes without middleware skip
async/awaitentirely, returning JSON inline with zero Promise allocation - Pipeline compilation happens once at startup, not per-request