-
Notifications
You must be signed in to change notification settings - Fork 0
Controllers and Decorators
Tanzim Hossain edited this page Apr 28, 2026
·
4 revisions
Class routes live across @nextrush/decorators (metadata), @nextrush/di (construction), and @nextrush/controllers (discovery + HTTP binding). @nextrush/controllers re-exports the pieces you typically import together.
Docs: Class-based guide, Guards concept.
pnpm add @nextrush/di @nextrush/decorators @nextrush/controllers{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Entry file:
import 'reflect-metadata';import { Controller } from '@nextrush/decorators';
@Controller('/users')
class UserController {
/* routes mount under /users */
}| Decorator | Verb |
|---|---|
@Get(path?) |
GET |
@Post(path?) |
POST |
@Put(path?) |
PUT |
@Patch(path?) |
PATCH |
@Delete(path?) |
DELETE |
@Head(path?) |
HEAD |
@Options(path?) |
OPTIONS |
@All(path?) |
Any |
Default path is '/' relative to the controller prefix.
@Controller('/users')
class UserController {
@Get()
findAll() {}
@Get('/:id')
findOne() {}
@Post()
create() {}
}@Get('/', { statusCode: 200, description: 'List users' })
findAll() {}
@Redirect('/new-path', 301)
@Get('/old-path')
legacy() {}
@SetHeader('Cache-Control', 'no-store')
@Get()
data() {}| Decorator | Extracts |
|---|---|
@Body(), @Body('key')
|
Body |
@Param(), @Param('id')
|
Route params |
@Query(), @Query('page')
|
Query |
@Header(), @Header('name')
|
Headers |
@Ctx() |
Full context |
@Req(), @Res()
|
Raw platform objects |
@Get('/:id')
findOne(@Param('id') id: string, @Query('include') include?: string) {
return { id, include };
}
@Post()
create(@Body() body: { name: string }) {
return body;
}@Get('/:id')
findOne(@Param('id', { transform: Number }) id: number) {
return { id };
}
@Post()
async create(@Body({ transform: schema.parseAsync }) body: MyType) {
return body;
}@Get()
list(
@Query('page', { required: false, defaultValue: '1' }) page: string,
) {
return { page };
}Guards return boolean (or promise of boolean). Controller-level guards run before method-level guards.
sequenceDiagram
participant G1 as Class guards
participant G2 as Method guards
participant H as Handler
G1->>G1: all pass?
alt fail
G1-->>Client: 403
else pass
G2->>G2: all pass?
alt fail
G2-->>Client: 403
else pass
G2->>H: invoke
end
end
import type { GuardFn } from '@nextrush/decorators';
const AuthGuard: GuardFn = async (ctx) => {
const token = ctx.get('authorization');
if (!token) return false;
ctx.state.user = verifyToken(token);
return true;
};import { Service } from '@nextrush/di';
import type { CanActivate, GuardContext } from '@nextrush/decorators';
@Service()
class RoleGuard implements CanActivate {
constructor(private roles: RoleService) {}
async canActivate(ctx: GuardContext): Promise<boolean> {
return this.roles.isAdmin(ctx.state.user);
}
}const RequireRole = (role: string): GuardFn => async (ctx) =>
ctx.state.user?.role === role;@UseGuard(AuthGuard)
@Controller('/users')
class UserController {
@Get()
findAll() {}
@UseGuard(RequireRole('admin'))
@Delete('/:id')
remove() {}
}GuardContext exposes method, path, params, query, body, headers, state, and get() — no response helpers; guards only approve or deny.
import { controllersPlugin } from '@nextrush/controllers';
const app = createApp();
const router = createRouter();
app.plugin(
controllersPlugin({
router,
root: './src',
prefix: '/api/v1',
debug: true,
}),
);
app.route('/', router);
listen(app, 3000);app.plugin(
controllersPlugin({
router,
controllers: [UserController, PostController],
}),
);| Option | Role |
|---|---|
router |
Target router |
root |
Scan directory |
prefix |
Global URL prefix |
controllers |
Explicit classes instead of scan |
include / exclude
|
Glob filters |
debug |
Log discoveries |
container |
Custom DI container |
| Error | Meaning |
|---|---|
DiscoveryError |
Scan failed |
GuardRejectionError |
Guard returned false |
MissingParameterError |
Required decorator input missing |
ParameterInjectionError |
Param extraction failed |
Handlers may return serializable data (JSON response) or throw HttpError. Return nothing when you write to ctx manually.
@Get()
findAll() {
return [{ id: 1 }];
}
@Get('/:id')
async findOne(@Param('id') id: string) {
const row = await db.find(id);
if (!row) throw new NotFoundError();
return row;
}
@Get('/stream')
stream(@Ctx() ctx: Context) {
ctx.status = 200;
ctx.set('Content-Type', 'text/event-stream');
}NextRush · MIT License · Docs · Issues