-
-
Notifications
You must be signed in to change notification settings - Fork 7.7k
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
Global prefix for a module / route tree #255
Comments
I'm also looking for something like this , It would be awesome to have a Control of Module prefix |
I agree, this will help keep our applications modular. At the moment I must specify my controller base as /api/users, /api/posts, /api/news etc. It's only a small inconvenience for small apps, but a redundancy nevertheless. |
I think there should be a |
A reference/example on pure Javascript + Express of how to do it: It's quite important with the issue #257 because in some cases you have nested elements: \book:bookHash\chapter:chatperHash And you want to test that bookHash exists (into the BookController) and also chapterHash exists (into the ChapterController) |
The issue is that the modules are not always just a tree. Sometimes they look more like a graph, for example, there might be a module, |
@kamilmysliwiec I don't know if it's a good or bad practice, but Angular has the ability of nested routes. It's not obligatory and is easy to have a workaround, of course, but could be a good improvement. |
@gelito nested routes are a good idea, but I can't see a good way to achieve this now. As I said above, module prefix may cause a lot of issues 🙁 |
Understood! BTW great job for all! |
isn't there any hack or some sort of middleware that would let us do that? |
What is the possible issue with a module prefix? |
@wbhob i have explained it below ^ |
Would it be possible to separate routing/controllers and things like services? The same way it's done in Angular for example, where you declare where to mount which modules into the routing tree but you can import and use a module in another without using any of it's routes? |
@kamshak hmm.. any ideas about the API? I'm trying to imagine how we could do it well |
I think this will help |
@shekohex haha 😄 well, I know how it's working in Angular, I was asking about the Nest API proposal |
Hahah 😂, sorry for misunderstand @Controller('foo') class Foo { }
@Controller('bar', Foo) class Bar { } then if I checked the Bar class route now it will be export function Controller(prefix?: string, parentController?: Controller): ClassDecorator {
let path = isUndefined(prefix) ? '/' : prefix;
if(parentController) {
const parentPath = Reflect.getMetadata(PATH_METADATA, parentController);
path = parentPath + '/' + path;
}
return (target: object) => {
Reflect.defineMetadata(PATH_METADATA, path, target);
Reflect.defineMetadata(PARENT_ROUTE, parentController, target);
};
} However, I have it now working , but I think it is not the best solution for this issue |
@shekohex honestly, it's the best idea for now heh |
I think it's just a Hack 😄
@Controller('posts')
export class PostsController {
@Get(':id')
getPosts(@Param('id') id: number) {
...
//go to database and returing the post
...
}
}
@Controller('comments', PostsController)
export class CommentsController {
@Get()
getPostComments() {
// now I need to get the controle of the post
// or post Id, to get it's comments
}
} so any Help in this ? |
|
@kamilmysliwiec: Will try to sketch out a quick consumer API. The challenge I see at the moment is that with the more declarative style you might loose some of the ease of the Decorators. (e.g. you'd always have to wire it up somewhere into the routing tree which might be tricky using decorators) |
This way, more options can be added later without breaking changes: @Controller('comments', {
parent: ParentController,
// OR THIS (preferably make both available)
parents: [
// In hierarchical order of importance
ParentController,
OtherParentController
]
})
export class CommentsController {
@Get('/child')
getPostComments() {
// implementation
}
} In my ideal world, though, the whole decorator metadata would look like this: @Controller({
path: '/comments', (force the leading slash)
components: [
// If someone wanted to scope them here, like
// `providers` in Angular
],
parent: ParentController,
// OR THIS (preferably make both available)
parents: [
// In hierarchical order of importance
ParentController,
OtherParentController
]
})
export class CommentsController {
@Get('/child')
getPostComments() {
// implementation
}
} |
good idea 👍 describe('RouterTreeFactory', () => {
it('should make a router tree', () => {
@Controller('grandpa') class GrandPa { }
@Controller('parent') class Parent { }
@Controller('child') class Child { }
const routerTree: IRouterTree = [{
parent: GrandPa,
childs: [Parent]
},
{
parent: Parent,
childs: [Child]
}];
appConfig.createRouterTree(routerTree);
const parentPath = Reflect.getMetadata('path', Parent);
const childPath = Reflect.getMetadata('path', Child);
expect(parentPath).to.be.eql('grandpa/parent');
expect(childPath).to.be.eql('grandpa/parent/child');
});
}); here is you don't have to edit async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
const routerTree = [{
parent: CatsController,
childs: [DogController, BirdsController]
}];
await app.createRouterTree(routerTree);
await app.listen(3000);
} |
but now we have a small problem , let's imagine we have the same 2 controllers ...
//simple routes tree
export const routesTree = [{
parent: CatsController,
childs: [DogsController]
}];
...
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
//should give me an validation error when I hit `cats/dogs`
findOne(@Param('id', new ParseIntPipe()) id) {
// logic
}
}
...
@Controller('dogs')
export class DogsController {
constructor(private readonly dogsService: DogsService) { }
@Get()
async findAll() {
return await this.dogsService.findAll();
}
} and It will also throw |
I like the proposed approach with passing the parents controllers into the |
@kamilmysliwiec maybe 😄 The Problem is the Parent Controller doesn't know that he Has a childs !I mean by that It , If the Parent Controller Has a 2 Children's , Then the Parent Should HAVE the ability to Remap it's routes to its children's . @Controller('dogs')
export class DogsController {
constructor(private readonly dogsService: DogsService) { }
@Get()
async findAll() {
return await this.dogsService.findAll();
}
}
...
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
// `/cats/`
@Get()
// this should be also remapped to `/cats/dogs` and the target should be `DogsController.findAll`
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
// `/cats/:id/`
@Get(':id')
// and this should be also remapped to `/cats/:id/dogs`
findOne(@Param('id', new ParseIntPipe()) id) {
// logic
}
} and so on.. |
It sounds like a big mistake. Parent's shouldn't know about they childs at all! Like in OOP, please do not reinvent the wheel. Btw, your example is mysterious, you're talking about Dogs Controller without showing it, can you improve it? |
I'm with @shekohex on this, parent should define child - not the other way round (that would be a bit strange, you want to include a module from somewhere and have to modify it just to mount it into your routing tree). Regarding
Yes, but I'd argue that routes are composition not inheritance. E.g. Animal class does not need to know that there are Dogs, Cats and Dolphins but the Car class absolutely needs to know about it's engine, wheels and fuel! To build the routing tree separately from the controllers you could do this: /*
This controller specifies an explicit route tree. Routes from the child modules
are mounted into the module's routing tree.
*/
@Module({
modules: [ UsersModule, AdminModule ],
controllers: [ HealthCheckController ],
routes: {
path: '/',
// NOTE: In theory we could add more stuff here (e.g. Middleware)
children: [
{ path: '', routes: HealthCheckController },
{ path: '/users', routes: UsersModule },
{ path: '/admin', routes: AdminModule }
// This also allows to mount another express app, e.g.
{ path: '/external', mount: express() }
]
}
}) export class AppModule { }
@Controller()
export class HealthCheckController() {
@Get('/healhtz') returnNothing() { }
}
/*
Each module, e.g. the Admin Module exports a routing tree, too.
If you do not speficy a routing configuration the routes are mapped by the annotations.
If you do specify routes it will always overwrite the component routes though.
*/
@Module({
controllers: [ DashboardController, AnalyticsController ]
}) export class AdminModule { }
@Controller('/dashboard')
export class DashbaordController {
@Get('/totalSales') totalSales() {
return 1230;
}
}
@Controller('/analytics')
export class AnalyticsController {
@Get('/users/:id') activeUsers() {
return 1230;
}
} Resulting Tree:
The issue i see is that when you're mixing patterns it gets a bit confusing. When you allow to specify things via decorator as well I believe this is why e.g. the Angular Router does not use Annotations - but i'm not 100% sure this is the reason. Maybe there is an elegant way to solve this problem. |
@cojack , I think you may miss that part
I don't know if I can't make It clear to you , But it's just the simple way to make the tests always green, I mean by
That The ...
export interface IRouter {
parent: Controller,
childs: Array<Controller>
}
export interface IRouterTree extends Array<IRouter> {}
export interface IRouterTreeFactory {
buildRouterTree(tree: IRouterTree);
}
...
export class RouterTreeFactory implements IRouterTreeFactory {
public async buildRouterTree(tree: IRouterTree): {
tree.forEach(route => {
const parentRoute = Reflect.getMetadata(PATH_METADATA, route.parent);
route.childs.forEach(child => {
const childRoute = Reflect.getMetadata(PATH_METADATA, child);
let newRoute = parentRoute + '/' + childRoute;
Reflect.defineMetadata(PATH_METADATA, newRoute, child);
});
})
}
} btw, I updated the example |
Hi @kamshak 😃
do You mean when I mention to use |
@kamshak I like your proposal, but we have to keep in mind that Nest is not only for the REST API's. In the incoming release, you'll be able to create an application context (without running any HTTP server behind) therefore Based on your suggestion, I think we should consider sth like this: @Module({
modules: [
UsersModule,
AdminModule,
RouterModule.configure({
path: '/',
children: [
{ path: '/users', routes: UsersModule },
{ path: '/admin', routes: AdminModule },
]
})
],
controllers: [HealthCheckController],
})
export class AppModule { } btw @wbhob it'd resolve the issue with module prefix 🙂 |
hmm, this syntax opens few new possibilities: @Module({
modules: [
UsersModule,
AdminModule,
InterceptorsModule.use(
InterceptorX,
InterceptorY,
),
GuardsModule.use(
GuardX,
GuardY
),
MiddlewaresModule.configure(
{
middleware: MiddlewareX,
forRoutes: []
}
)
],
controllers: [HealthCheckController],
})
export class AppModule { } = bind module scoped guards & interceptors etc etc hmm.. but for guards, interceptors, exception filters and pipes this might be redundant (why not to reuse available decorators?)... However, for middlewares & router configuration it should fit great |
I've opened a PR. This is sort of what we're talking about, but in a more nest-friendly way. My intended usage is like this: @Module({
path: '/child-route'
})
export class MyChildModule { } Essentially, at runtime, it reflects the parent module's paths back onto each controller. |
Hi guys, I looked into this with a lot of interest. Kamil did not approved the PR, for reasons I can understand. We're about to split our API in 3 modules and it'd be lovely to be able to have a global prefix for all controllers in each module (eg /module1, /module2, /module3...). |
@VinceOPS This is how we've done it so far
|
@VinceOPS looks good, It is the proper way? |
@br0wn |
@br0wn hey! Thanks for sharing your approach. I must consider it, but it looks nice. Thanks guys! |
Actually @wbhob proposed a very good solution for this , but it rejected.
|
continue with this PR #389 |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Hi,
there is an older issue, which got implemented: #40 - allowing to
setGlobalPrefix('api')
on the app instance.
This is a very neat feature and since I'm looking to create some sort of a "route tree", it would be great to set some "route extending strategy".
Say I want to create a route tree like this:
-> api (from app.setGlobalPrefix)
--> /users (UsersModule with prefix 'users' for all of it's 'children' modules and controllers)
---> /posts (PostsModule with prefix 'posts' for all of it's 'children' modules and controllers)
----> /comments (Now I could declare a CommentsController with route 'comments' and this controller's route would be composed from all of the ancestors above, in this case: 'api/users/posts/comments'
----> /upvotes (this one would then be 'api/users/posts/upvotes')
that means I'd like to somehow set the 'module route prefix' for all of it's children's modules & controllers.
If there's already a way to achieve this, please point me in the right direction, otherwise it would be pretty nice to have this.
Thanks
The text was updated successfully, but these errors were encountered: