Routing

How MiiaJS matches URLs to handlers — static routes, dynamic parameters, wildcards, and route compilation.

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:

  1. Static lookup — O(1) Map check for routes without parameters (e.g. /users, /health)
  2. Trie traversal — walks the trie for routes with :param or * wildcard segments
  3. 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 Map for 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.query is accessed
  • Sync fast path — routes without middleware skip async/await entirely, returning JSON inline with zero Promise allocation
  • Pipeline compilation happens once at startup, not per-request