Before / after

The same feature hand-wired in plain Nitro vs. with roost — proven identical in behavior.

Here's the same projects feature, written two ways: idiomatic plain Nitro + awilix (the "before"), and with roost (the "after"). They boot and behave identically — same 401/403/200/400, same request-scoped tenant isolation. That parity is the point: roost is an ergonomics layer, not a capability gap. The difference is the wiring you stop writing.

1. The route file

Every protected, validated route repeats the whole dance by hand. With roost it's a decorated method — roost generates the delegate for you:

import { defineEventHandler, createError } from 'h3'
import { asValue } from 'awilix'
import { ZodError } from 'zod'
import { container } from '../../container'
import { buildCtx } from '../../ctx'
import { memberGuard } from '../../guards'
import { withAudit } from '../../audit'
import { ProjectsService } from '../../projects/projects.service'
import { createProjectSchema } from '../../projects/dto'

export default defineEventHandler(async (event) => {
  const ctx = await buildCtx(event)
  memberGuard(ctx) // guard, by hand
  try {
    return await withAudit(ctx, () => {
      // audit wrap, by hand
      const body = createProjectSchema.parse(ctx.body) // validate, by hand
      const scope = container.createScope() // request scope, by hand
      scope.register({ requestContext: asValue({ tenantId: ctx.tenantId, user: ctx.user }) })
      return scope.resolve<ProjectsService>('projectsService').create(body.name)
    })
  } catch (err) {
    if (err instanceof ZodError) throw createError({ statusCode: 400, data: err.issues }) // map, by hand
    throw err
  }
})

The guard is class-level (inherited by every method), the audit interceptor is one global config line, and ZodError → 400 is the built-in filter. Forget the guard / audit / error-map on a hand-written route and it silently misbehaves; here they can't be forgotten.

2. The DI composition root

The exact same three providers — same lifetimes, same dependency graph — expressed as hand-written registrations you maintain vs. a decorator on the class that owns the scope:

import { createContainer, asFunction, Lifetime } from 'awilix'
import { ProjectStore } from './projects/projects.store'
import { ProjectsRepo } from './projects/projects.repo'
import { ProjectsService } from './projects/projects.service'

export const container = createContainer({ strict: true })

container.register({
  projectStore: asFunction(() => new ProjectStore(), { lifetime: Lifetime.SINGLETON }),
  projectsRepo: asFunction(
    ({ projectStore, requestContext }) => new ProjectsRepo(projectStore, requestContext),
    { lifetime: Lifetime.SCOPED },
  ),
  projectsService: asFunction(({ projectsRepo }) => new ProjectsService(projectsRepo), {
    lifetime: Lifetime.SCOPED,
  }),
  // a second feature → six more lines, by hand…
})

There's no composition root to maintain: the lifetime and the dependency graph live on the classes, and roost aggregates the container. 4 providers or 400, nobody hand-edits it.

The delta

Hand-rolledroost
Route files you writeone per endpoint, ~15–25 lines of wiring each0 (generated)
DI container you maintainone file, edited on every provider change0 (generated)
Imports per feature fileevery dependency, by handnone — decorators/types + your own pieces are auto-imported
Cross-cutting (guard/audit/error-map)threaded by hand into every handlerdecorators + globals
Add an endpointnew route file + wire guard/audit/scope/validateadd one decorated method
Forget a guard / audit on a route?silently public / un-auditedclass-level + global, can't be missed
Runtime reflectionnonenone (AST read at build time)
Behavior← identical →identical

The tell

Notice the repeated guard / scope / audit / error-map in every hand-written handler. The obvious next step is to factor that shared dance into a runRequest(token, method, opts) helper — at which point you've hand-written roost's route(), and the container is roost's generated DI manifest. roost is what you converge on; it just generates it instead of asking you to maintain it.

Copyright © 2026