Controllers
Controllers group related routes into a class — useful for resource-heavy endpoints (UserController, PostController) where every method shares the same prefix and middleware. Controllers are decorator-based and resolve dependencies through the DI container.
import { Controller, Get, Post, Put, Delete, Middleware, router } from '@rudderjs/router'
import { Injectable } from '@rudderjs/core'
import { RequireAuth } from '@rudderjs/auth'
import { UserService } from '../Services/UserService.js'
@Controller('/api/users')
@Injectable()
class UserController {
constructor(private readonly users: UserService) {}
@Get('/')
async index(_req, res) {
return res.json({ data: await this.users.all() })
}
@Get('/:id')
async show(req, res) {
const user = await this.users.find(req.params.id)
if (!user) return res.status(404).json({ message: 'Not found' })
return res.json({ data: user })
}
@Post('/')
@Middleware([RequireAuth()])
async store(req, res) {
const user = await this.users.create(req.body as any)
return res.status(201).json({ data: user })
}
@Put('/:id')
async update(req, res) {
return res.json({ data: await this.users.update(req.params.id, req.body as any) })
}
@Delete('/:id')
async destroy(req, res) {
await this.users.delete(req.params.id)
return res.status(204).send('')
}
}
router.registerController(UserController)The controller is registered via router.registerController(UserController) — typically in routes/api.ts or routes/web.ts. Generate stubs with pnpm rudder make:controller User.
Decorators
| Decorator | Target | Description |
|---|---|---|
@Controller(prefix?) |
class | Route prefix for every method |
@Get(path) |
method | GET |
@Post(path) |
method | POST |
@Put(path) |
method | PUT |
@Patch(path) |
method | PATCH |
@Delete(path) |
method | DELETE |
@Options(path) |
method | OPTIONS |
@Middleware(handlers[]) |
class or method | Apply middleware |
@Controller and method decorators compose: @Controller('/api/users') + @Get('/:id') registers GET /api/users/:id.
Constructor injection
Controllers must be @Injectable() to participate in DI. Constructor parameters are resolved from the container — services, repositories, anything bound in a service provider:
@Controller('/api/posts')
@Injectable()
class PostController {
constructor(
private readonly posts: PostService,
private readonly cache: CacheStore,
private readonly notifier: NotificationManager,
) {}
}reflect-metadata must be imported once at the entry point (bootstrap/app.ts) for parameter types to survive compilation. Scaffolded apps already include it.
Method-level parameter decorators are not supported — esbuild and Vite drop method-level metadata. If you need to inject something into a single method, take it as a constructor dependency or use
app().make(Token)inline.
Middleware
Use @Middleware([...]) on the class, the method, or both. Class middleware runs first:
@Controller('/api')
@Middleware([logRequest]) // runs first for every method
class Ctrl {
@Get('/private')
@Middleware([RequireAuth()]) // runs second, only this method
private(req, res) { /* ... */ }
}For middleware that runs on every web or every api route, register it on the group instead — see Middleware.
Controllers and views
A controller can return a view directly, the same as a fluent route:
import { view } from '@rudderjs/view'
@Controller('/dashboard')
@Injectable()
class DashboardController {
constructor(private readonly stats: StatsService) {}
@Get('/')
async index() {
return view('dashboard', { stats: await this.stats.summary() })
}
}Returning a view from a controller is the canonical pattern for full-page web routes — middleware, data fetching, and rendering live in the same method.
Controllers vs. fluent routes
Both approaches use the same router. Pick whichever reads better:
| Fluent | Controller | |
|---|---|---|
| Lightweight handlers, one-off endpoints | ✓ | |
| Resource with shared prefix + middleware | ✓ | |
| Constructor-injected dependencies | (use resolve() inline) |
✓ |
| File organization | One routes/api.ts |
One file per controller |
You can mix both in the same app. The framework treats them identically once registered.
Pitfalls
reflect-metadatamissing. Decorator metadata silently disappears. Addimport 'reflect-metadata'tobootstrap/app.ts.- Forgetting
@Injectable(). The container can't auto-construct a class without it. The error is a clearCannot resolve UserServiceat boot. - Forgetting
router.registerController(...). The class compiles but no routes show up. Runpnpm rudder route:listto confirm. - Method-level decorators relying on
design:paramtypes. Vite/esbuild strip method-level metadata. Use constructor injection instead.