nest
nest copied to clipboard
Add dynamic routing
I'm submitting a...
[ ] Regression
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
Current behavior
At the moment there is no way to register a route dynamically except by using the internal HTTP / Fastify / Express instance (related #124):
const expressInstance = express();
expressInstance.use(morgan('dev'));
const app = NestFactory.create(ApplicationModule, expressInstance);
expressInstance.get('/foo', function (req, res) {
res.send('bar');
})
The problem with this approach is, Nest does internally not recognize this route. Therefore it will not show up in the e.g. swagger integration.
Expected behavior
I wish to have a dynamic router as part of the public Nest API, or at least a way to tell Nest to use an external route inside its router registry.
What is the motivation / use case for changing the behavior?
nestjs/terminus routes do not get registered because it directly modifies the HTTP instance. Therefore there is no Swagger integration or compatibility with middleware.
Related: nest/terminus#32 nest/terminus#33
other integrations such as @zMotivat0r for example uses this rather "hacky" workaround (sorry :P): https://github.com/nestjsx/crud/blob/e255120b26dd8ca0eae9c7ec9dac4f893051f447/src/crud.module.ts#L5-L15 or nest-router by @shekohex uses what I would consider the internal API:
https://github.com/shekohex/nest-router/blob/3759c688b285792d7889b52f75fffb0e86d4dd54/src/router.module.ts#L53-L56
Environment
Nest version: 5.x.x
For Tooling issues:
- Node version: XX
- Platform:
Others:
What exactly would you like to see in the Nest core actually? Do you have any proposals on how the API could potentially look like? Just to clarify, I fully understand the use-case :)
@kamilmysliwiec Maybe something like this?
const appRoutes: Routes = [
{
path: 'hero',
method: RequestMethod.POST,
use: (heroService: HeroService) => heroService.getAll(),
inject: [HeroService],
// This option would ignore the `use` option.
// What it basically does is telling Nest that the 'hero'-path
// is registered in the underyling httpServer.
callThrough: true
},
];
@Module({
imports: [
RouterModule.forRoot(
appRoutes,
{enableTracing: true} // Future possibilities :)
)
],
...
})
In general we should thrive for implementing every feature which is accessible through decorators, also a dynamic approach, as shown in this example. As you know, decorators are cool for clean code, but they lack in extensibility (I think you mentioned something similar in a past issue about Java Spring)
Hi @BrunnerLivio i would agree with you about the issue of external package sometimes have some sort of * cough cough * poor integration with each others, as an example you mentioned the swagger pkg, btw the first issue in nest-router was Swagger integration https://github.com/shekohex/nest-router/issues/3 , so your API is nice
const appRoutes: Routes = [ { path: 'hero', method: RequestMethod.POST, use: (heroService: HeroService) => heroService.getAll(), inject: [HeroService], // This option would ignore the `use` option. // What it basically does is telling Nest that the 'hero'-path // is registered in the underyling httpServer. callThrough: true }, ]; @Module({ imports: [ RouterModule.forRoot( appRoutes, {enableTracing: true} // Future possibilities :) ) ], ... })
so do you think we actually need that in the core ?
oh, i think nest router package can do something like that ! it's already have a full access to the module containers, so we can get a ref to any service from any module. https://github.com/shekohex/nest-router/blob/3759c688b285792d7889b52f75fffb0e86d4dd54/src/router.module.ts#L17
so what do you think ? but since nest-router was created in first place to solve other problem, nesting routes see https://github.com/shekohex/nest-router/issues/43 is that possible ? anyway i could help in either way here in the core or externally in another pkg or in nest-router itself :smile:
@shekohex thanks a lot for your answer!
so do you think we actually need that in the core ?
I guess it should atleast be at the same place where the routing is handled, therefore nestjs/core I guess?
In my opinion the functionality of your router or similar should be part of the framework. As already hinted at I would like to see every decorator functionality also as non-decorator configurable option. Therefore a public API of the router would be quite handy!
ok, we can work on that
my opinion the functionality of your router or similar should be part of the framework.
Yeah, that was a long opened issue/discussion in nest-router
see https://github.com/shekohex/nest-router/issues/19 it's about year ago !
i think we can do better, what about that, since most of the users who will use this api most of them will be 3rd party lib creators, we can make it even lower level api, instead of use: (someService: SomeService) => someService.someMethod(),
, we give them a reference to the underlying module container itself, or an express-like (req, res, next, container) => {...}
function api.
the reason of that, it would make it easier to anyone want to have a full control over the underlying router. there is a lot of options there, but really i would love the idea of
to see every decorator functionality also as non-decorator configurable option
since i love to use nest in functional manner.
i know i know that nest built around a lot of concepts like DI, IoC, Modules ...etc, but really i love to use nest with just only functions.
instead of use: (someService: SomeService) => someService.someMethod(),, we give them a reference to the underlying module container itself, or an express-like (req, res, next, container) => {...} function api.
Great input!
New Proposal
Router Module in @nest/core
// Route interfaces
interface RouteRegister {
path: string;
method?: RequestMethod;
}
interface RouteUse {
use: (req: any, res: any, next: Function, container: any) => Promise<any> | any;
}
interface RouteCallThrough {
callThrough?: boolean;
}
type RouteWithCallThrough = RouteRegister & RouteCallThrough & { callThrough: true };
type RouteWithUse = RouteRegister & RouteUse & { callThrough?: false | undefined | null };
type Route = RouteWithCallThrough | RouteWithUse;
// Module interfaces
interface RouterModuleOptions {
routes: Route[];
enableTracing?: boolean;
}
interface RouterOptionsFactory {
createRouterOptions():
| Promise<RouterModuleOptions>
| RouterModuleOptions;
}
interface RouterModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
name?: string;
useClass?: Type<RouterOptionsFactory>;
useExisting?: Type<RouterOptionsFactory>;
useFactory?: (
...args: unknown[]
) => Promise<RouterModuleOptions> | RouterModuleOptions;
inject?: unknown[];
}
// Router module implementation
export class RouterModule {
static forRoot(options: RouterModuleOptions): DynamicModule {
// Todo: Implement
return {
module: RouterModule,
};
}
static forRootAsync(options: RouterModuleAsyncOptions): DynamicModule {
// Todo: Implement
return {
module: RouterModule
}
}
}
Usage Synchonous
const routes: Route[] = [
{
path: '/underlying-route',
method: RequestMethod.GET,
callThrough: true,
},
{
path: '/new-route',
method: RequestMethod.GET,
use: (req, res, next, container) => 'test',
}
];
@Module({
imports: [
RouterModule.forRoot({
routes,
enableTracing: true
}),
],
})
class AppRoutes { }
Usage Asynchronous
class RouterModuleFactory implements RouterOptionsFactory {
constructor(private heroController: HeroController) { }
createRouterOptions(): RouterModuleOptions {
return {
routes: [
{
path: '/new-route2',
use: (req, res) => this.heroController.get(req, res),
}
]
}
}
}
@Module({
imports: [
RouterModule.forRootAsync({
useClass: RouterModuleFactory,
inject: [HeroController]
})
],
})
class AppRoutes { }
Disclaimer
The interface Route
is kind of complicated. This is due to; if the user uses callThrough: true
he mustn't use use
. If callThrough: false
or undefined the user can use use
. Therefore the following would not be possible:
const routes = Route[] = [
{ path: '/test', callThrough: true, use: () => 'test' } // ERROR
]
@kamilmysliwiec @shekohex what do you think? :)
use
doesn't seem descriptive for what it does.
And that's still not a dynamic API as you can only add routes inside decorators.
You should be able to import the container in an injectable and add a route from there as well.
@marcus-sa thanks for the feedback.
use doesn't seem descriptive for what it does.
I've used use
because Express uses app.use
too for middleware definitions
And that's still not a dynamic API as you can only add routes inside decorators. You should be able to import the container in an injectable and add a route from there as well.
I see your point. Maybe an additional RouterService
would do the trick? I would still keep the RouterModule
though because it is a nice way to define routes at one central point, similar to Angular router. RouterService
could then be an exported provider from RouterModule
.
RouterModule
is still pretty dynamic, because you can import providers using forRootAsync
- but in some situations injecting a RouterService
is just more convenient. Question is; should we support both? This could maybe raise inconsistencies into a users app if he/she starts using RouterModule
, RouterService
and @Controller()
at the same time.
@BrunnerLivio yes, but use
is an inconsistent name in MAF's (module architecture framework) as by usage it's actually a FactoryProvider
, so it should be useFactory
instead for naming consistency within the entire Nest framework :)
@marcus-sa I thought about that too, but it is not actually a factory, its a router handle. On top of that the way I proposed you can not inject anything into the use
function, you would have to use the moduleContainer
parameter which is given to the use
function, or forRootAsync
on the RouterModule
. So the functionality differs from useFactory
. But I agree that use
could be misleading, so I am open for a better name. Maybe useHandler
or handler
?
@BrunnerLivio yeah I just noticed that too and was about to propose the same useHandler
or handler
in the beginning
That's pretty good api, it's actually similar to the nest-router
module API.
should we support both? This could maybe raise inconsistencies into a users app if he/she starts using RouterModule, RouterService and
@Controller()
at the same time.
yup, if we need to make things dynamic and in the same time easy to use, we should also support RouterService
.
This could maybe raise inconsistencies...
well, that's a good question here, but i really can't imagine that issue, since we will make nest knows all the routes either they are registered form RouterModule
or using @Controller(..)
, or maybe better, if the user used callThrough
for example, nest also should know that we have that route, which is the idea of this issue in 1st place.
I am open for a better name. Maybe useHandler or handler?
useHandler
:+1:
@zMotivat0r What are your thoughts on this, since you can benefit with @nestjsx/crud
from this too? Any other recommendations from your side before we proceed?
Edit: By the way; I think a RouteService which allows to get and set all routes during runtime could also heavily benefit the codebase of @nestjs/swagger
.
@BrunnerLivio thanks for asking.
useHandler
:+1:
What are your thoughts on this, since you can benefit with @nestjsx/crud from this too? Any other recommendations from your side before we proceed?
well, yes. I've started implementing a dynamic module for @nestjsx/crud
and it will be much easier to do with Nest dynamic routing. The main point here is to have all decorators functionality also as non-decorator way, including custom route decorators (if not, I want to here why) and pipes.
And for sure, this feature is one of the cool features and this will help to open more possibilities for Nest.
I had a private chat with @kamilmysliwiec and I want to keep you sync.
I started implementing the proposal and came across some problems. If you know NestJS behind the scene you realize that the framework has parts which are being provided inside the application context “dependency injection” e.g. HttpModule
or outside “facade” e.g. NestApplicationContext
.
The current router handler is fully part of the “facade”. The problem is that it is hard to call / modify the “facade” from the “DI”. Therefore we came to the conclusion we need to refactor the router functionality into the application context, so this issue is feasable without any hacky workaround.
We will then import the RouterModule
as one of the CoreModule
to prevent any breaking changes of the public API.
At the moment both Kamil and I are really busy, so this issue may take some time - except someone else takes on the task :)
I'm not sure about the status of this issue, but this is a really basic framework feature.
Usual API looks like this (router should be injectable provider):
router.addRoute('/login/some-action', LoginController.someAction)
url = router.getUrl(LoginController.someAction, {someParam: 'some value'})
Routes are decoupled from controllers and can be defined in one place. This allows us to keep the app maintainable even with many routes.
Note that in some frameworks (e.g Flask) we'd pass a string 'LoginController.someAction'
instead of the method itself, but passing the method allows for code completion and IDE checks.
Routes are decoupled from controllers and can be defined in one place. This allows us to keep the app maintainable even with many routes.
This is completely contrary to the ideas and principles of Nest. For this, you can use Koa/Express.
This is completely contrary to the ideas and principles of Nest
I consider Nest to be the best node framework because of it's built in DI and SRP approach. Could you please elaborate how is separation of responsibility completely contrary to its principles? Because I honestly thought that this kind of decoupling and maintainability is what you aim for.
I just stumbled upon a use case. I have highly modular system and want to prevent a possibility for routes to coincide between modules. So I wanted to pass a "root route" of a controller with module options (when instantiating the module from AppModule), but this doesn't seem to be possible, since route is hardcoded in a @Controller
decorator. Please make it possible to be dynamic.
Hello
My case is also connected with dynamic routing Currently, my application is generated from metadata prepared in the case. I could for any entity generate a controller, service etc but I would prefer to redirect multiple routes to one controller.
i.e. dogs, cats, birds go to the CommonControllerForAllCases controller
for now the only solution I have found: 1 in the Get decorator give an array of all paths to be handled by this controller: @Get (['coffees / get', 'dogs / get']) 2 Perform service in the interceptor
Am I right, can this problem be solved better?
Hello!
Is there any updates on this issue?
I am developing an application that proxies photos and resizes them if needed For example: /image/test.jpg will return the original image /image/test.jpg?width=300&height=400 will return a resized image
I decided to make it a module so that other developers can use it in their projects. And I want to give them the ability to configure the url prefix ("image" for now). Right now I can't do it in a pretty way and am forced to use a dirty hack like this https://github.com/nestjsx/crud/blob/e255120b26dd8ca0eae9c7ec9dac4f893051f447/src/crud.module.ts#L5-L15
Another option that has been found for changing that path is by making a custom provider that sets the metadata of the class like the following:
{
provide: Symbol('CONTROLLER_HACK'),
useFactory: (config: StripeModuleConfig) => {
const controllerPrefix =
config.controllerPrefix || 'image';
Reflect.defineMetadata(
PATH_METADATA,
controllerPrefix,
ControllerForModule
);
},
inject: [MODULE_CONFIG_INJECTOR],
},
@jmcdo29 Thanks! This solution looks pretty nice and works great for me.
I used this example for adding routes dynamically to a NestJS controller.
It is not extremely flexible but does the job
1. define a factory for your controller
// mymodule.controller.ts
export function getControllerClass({ customPath }): Type<any> {
@Controller()
class MyController {
constructor(private service: MyService) { }
@Get([customPath])
async getSomething(@Res() res: Response) {
//...
}
}
return MyController
}
in your module
2. configure routes via the module
// mymodule.module.ts
@Module({})
export class MyModule {
static forRoot(config): DynamicModule {
const { customPath } = config;
const MyController = getControllerClass({ customPath })
return {
module: MyModule,
providers: [
MyService
],
exports: [MyService],
controllers: [MyController]
};
}
}
Another way to override a controller's decorators. This trick uses existing decorator methods, and does not require dynamic classes or mixins.
import { Controller, Post } from '@nestjs/common';
import { MyControllerClass } from './controllers';
// main route
Controller('new/route')(MyControllerClass);
// `doSomething` POST method (possibly sketchy)
Post('sub/route')(
MyControllerClass,
'doSomething',
Object.getOwnPropertyDescriptor(MyControllerClass.prototype, 'doSomething')
);
I am currently dynamically adding handles (get, post ...) to the handler using a function the code lookslike this: function file: import { METHOD_METADATA, PATH_METADATA } from '@af-be/common.constants'; import { isString } from '@af-shared/utils'; import { RequestMappingMetadata, RequestMethod } from '@nestjs/common'; import { TMethodDecorator } from '../constants/type';
const defaultMetadata: RequestMappingMetadata = { path: '/', method: RequestMethod.GET };
const requestMapping = (
target: TMethodDecorator,
metadata: RequestMappingMetadata = defaultMetadata
): void => {
const pathMetadata = metadata[PATH_METADATA];
const path = pathMetadata?.length ? pathMetadata : '/';
const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
const currentMetaData: string | Array
export function MetaDataRequestRegister(method: RequestMethod, target: TMethodDecorator,
path?: string | Array
using:
MetaDataRequestRegister(
RequestMethod.POST,
GetEntityControllersManager.prototype.getEntityHandle,
af/${EProjectTestRouterNames.projecttest}/${EProjectTestServiceName.EProjectTestEntityServiceName}/
+
${EProjectTestEndPointDBNames.GetAssigment}
);
export const HOST_METADATA = 'host';
export const PATH_METADATA = 'path';
export const SCOPE_OPTIONS_METADATA = 'scope:options';
export const METHOD_METADATA = 'method';
export type TMethodDecorator = (...data: Array
Un altro modo per ignorare i decoratori di un controller. Questo trucco utilizza metodi decoratore esistenti e non richiede classi dinamiche o mixin.
import { Controller, Post } from '@nestjs/common'; import { MyControllerClass } from './controllers'; // main route Controller('new/route')(MyControllerClass); // `doSomething` POST method (possibly sketchy) Post('sub/route')( MyControllerClass, 'doSomething', Object.getOwnPropertyDescriptor(MyControllerClass.prototype, 'doSomething') );
Following this approach it is possible to inject methods on the fly and then decorate them. I also managed to decorate the parameters to retrieve @Query etc etc.
for (const collectionOperation of Object.keys(
options.collectionOperations,
)) {
const config = options.collectionOperations[collectionOperation];
ApiController.prototype[collectionOperation] = function (query) {
return { Ciao: 'Mondo', query };
};
Get(config.path)(
ApiController,
collectionOperation,
Object.getOwnPropertyDescriptor(
ApiController.prototype,
collectionOperation,
),
);
Query()(ApiController.prototype, collectionOperation, 0);
}
return ApiController;
http://127.0.0.1:4000/api/items/sub/route?ciao=23
{
"Ciao": "Mondo",
"query": {
"ciao": "23"
}
}
thank you.
I used this example for adding routes dynamically to a NestJS controller.
It is not extremely flexible but does the job
1. define a factory for your controller
// mymodule.controller.ts export function getControllerClass({ customPath }): Type<any> { @Controller() class MyController { constructor(private service: MyService) { } @Get([customPath]) async getSomething(@Res() res: Response) { //... } } return MyController }
in your module
2. configure routes via the module
// mymodule.module.ts @Module({}) export class MyModule { static forRoot(config): DynamicModule { const { customPath } = config; const MyController = getControllerClass({ customPath }) return { module: MyModule, providers: [ MyService ], exports: [MyService], controllers: [MyController] }; } }
Is there anyway we could extend this to dynamically inject services into the controller? I have a module where I dynamically register routes which dynamically create controllers using getControllerClass, but Im wondering if theres anyway I could also pass service methods into the controller such that for given routes, my controller will call certain service methods
Here is my attempt using PATH_METADATA:
// my checkupModule.ts
...
@Module({
controllers: [QueueController],
providers: [CheckupService, QueueService],
exports: [CheckupService, QueueService],
})
export class CheckupModule extends ConfigurableModuleClass {
static register(options: typeof OPTIONS_TYPE): DynamicModule {
const { namespace } = options;
Reflect.defineMetadata(PATH_METADATA, namespace, CheckupController);
return {
module: CheckupModule,
imports: [
BullModule.registerQueueAsync({
name: `${namespace}_JOB`,
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const limiter = limiterConfig(
configService.get(`${namespace}_LIMITER`),
);
return { limiter };
},
}),
],
...super.register(options),
};
}
}
hacked PATH_METADATA: RELATIONSHIP
hacked PATH_METADATA: PRODUCT
...
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RoutesResolver] CheckupController {/PRODUCT}: +0ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/status, GET} route +1ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/pause, POST} route +0ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/resume, POST} route +0ms
...
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RoutesResolver] CheckupController {/PRODUCT}: +0ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/status, GET} route +1ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/pause, POST} route +0ms
[Nest] 37765 - 11/22/2022, 4:45:35 PM LOG [RouterExplorer] Mapped {/PRODUCT/resume, POST} route +0ms
Unfortunately, it appears the router builds out after all modules are registered. Both instances of CheckupController
use have the same path.
Is there a way to build out the controllers in each module as they are created?
Any updates on this? This is hands down the primary reason, why not to choose NestJS as your backend for dynamic integrations.