-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add dynamic routing #1438
Comments
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
so do you think we actually need that in the core ? oh, i think nest router package can do something like that ! so what do you think ? |
@shekohex thanks a lot for your answer!
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
Yeah, that was a long opened issue/discussion in 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 the reason of that, it would make it easier to anyone want to have a full control over the underlying router.
since i love to use nest in functional manner.
|
Great input! New ProposalRouter 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 Synchonousconst 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 Asynchronousclass 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 { }
DisclaimerThe interface const routes = Route[] = [
{ path: '/test', callThrough: true, use: () => 'test' } // ERROR
] @kamilmysliwiec @shekohex what do you think? :) |
|
@marcus-sa thanks for the feedback.
I've used
I see your point. Maybe an additional
|
@BrunnerLivio yes, but |
@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 |
@BrunnerLivio yeah I just noticed that too and was about to propose the same |
That's pretty good api, it's actually similar to the
yup, if we need to make things dynamic and in the same time easy to use, we should also support
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
|
@zMotivat0r What are your thoughts on this, since you can benefit with 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 |
@BrunnerLivio thanks for asking.
well, yes. I've started implementing a dynamic module for 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. 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 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 |
This is completely contrary to the ideas and principles of Nest. For this, you can use Koa/Express. |
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 |
Hello My case is also connected with dynamic routing i.e. dogs, cats, birds go to the CommonControllerForAllCases controller for now the only solution I have found: 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 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 const defaultMetadata: RequestMappingMetadata = { const requestMapping = ( export function MetaDataRequestRegister(method: RequestMethod, target: TMethodDecorator, using: MetaDataRequestRegister( export const HOST_METADATA = 'host'; |
Following this approach it is possible to inject methods on the fly and then decorate them. 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; {
"Ciao": "Mondo",
"query": {
"ciao": "23"
}
} thank you. |
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 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. |
Hi guys, I'm sharing my implementation for generating a dynamic router (controller) in NestJS. import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Req } from "@nestjs/common";
import { GetCollectionService } from "../services/get.collection.service";
import { GetItemService } from "../services/get.item.service";
import { SaveItemService } from "../services/save.item.service";
import { Calendar } from "../../calendar/models/calendar.entity";
type OperationConfig = {
entity: any;
method: 'get' | 'getItem' | 'post' | 'patch' | 'put' | 'delete';
path?: string;
}
const addMethodsAndDecorate = (controller: any, operations: OperationConfig[]) => {
for (const config of operations) {
switch (config.method) {
case 'get':
controller.prototype.findAll = function (query) {
return this.getCollectionService.getCollection({
entity: config.entity,
builder: { with: {} },
filters: [],
queryArgs: query,
});
};
Get(config.path)(controller, 'findAll', Object.getOwnPropertyDescriptor(controller.prototype, 'findAll'));
Query()(controller.prototype, 'findAll', 0);
break;
case 'getItem':
controller.prototype.findOne = async function (id: string) {
return this.getItemService.getItem(id, { entity: config.entity });
};
Get(':id')(controller, 'findOne', Object.getOwnPropertyDescriptor(controller.prototype, 'findOne'));
Param('id')(controller.prototype, 'findOne', 0);
break;
case 'post':
controller.prototype.create = function (body) {
const payload = {
...(body ?? {}),
tenant: { id: 2 },
};
return this.saveItemService.save(payload, { entity: config.entity });
};
Post(config.path)(controller, 'create', Object.getOwnPropertyDescriptor(controller.prototype, 'create'));
Body()(controller.prototype, 'create', 0);
break;
case 'patch':
controller.prototype.partialUpdate = async function (id: string, req) {
const item = await this.getItemService.getItem(id, { entity: config.entity });
if (!item) {
throw new Error('Not found');
}
const payload = req.body;
return this.saveItemService.save({ id: item.id, ...payload }, { entity: config.entity });
};
Patch(":id")(controller, 'partialUpdate', Object.getOwnPropertyDescriptor(controller.prototype, 'partialUpdate'));
Param('id')(controller.prototype, 'partialUpdate', 0);
Req()(controller.prototype, 'partialUpdate', 1);
break;
case 'delete':
controller.prototype.deleteOne = async function (id: string) {
const item = await this.getItemService.getItem(id, { entity: config.entity });
if (!item) {
throw new Error('Not found');
}
return this.userCreateService.delete(item);
};
Delete(':id')(controller, 'deleteOne', Object.getOwnPropertyDescriptor(controller.prototype, 'deleteOne'));
Param('id')(controller.prototype, 'deleteOne', 0);
break;
}
}
return controller;
}
const crudFactory = (namespace: string, operations: OperationConfig[]) => {
@Controller(namespace)
class CalendarController {
constructor(
public getCollectionService: GetCollectionService,
public getItemService: GetItemService,
public saveItemService: SaveItemService,
) {}
}
return addMethodsAndDecorate(CalendarController, operations);
}
export const CrudController = crudFactory('api/cals', [
{
method: 'get',
entity: Calendar,
},
{
method: 'post',
entity: Calendar,
},
{
method: 'patch',
entity: Calendar,
},
{
method: 'getItem',
entity: Calendar,
},
{
method: 'delete',
entity: Calendar,
}
])
// You can also take the generated class and extend it to add more methods.
export class MyNewController extends CrudController {
constructor(
public getCollectionService: GetCollectionService,
public getItemService: GetItemService,
public saveItemService: SaveItemService,
) {
super(getCollectionService, getItemService, saveItemService);
}
// ...add more methods
} |
I've been looking at Lazy loading modules which works well with some of the discussion above. It does not allow Controller routes to be added, something I'm interested in and looking at. Has anybody else looked at this? |
I'm submitting a...
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):
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
The text was updated successfully, but these errors were encountered: