nest icon indicating copy to clipboard operation
nest copied to clipboard

Add support for dynamically generating URLs

Open zetti-caletti opened this issue 3 years ago • 32 comments

Is there an existing issue that is already proposing this?

  • [x] I have searched the existing issues

Is your feature request related to a problem? Please describe it

I am wondering how to generate URLs in NestJS. For example in a Controller or Service. Something like in symfony routing. I found that there is an external package Nest-URL-Generator but it isn't that popular and has not many downloads. I am wondering how all the users of NestJS generate URLs in their application without hardcoding them? The Github API for example has at their API entrypoint a list full of URLs for their resources https://api.github.com/ I want to do the same, but without hardcoding all the URLs. Because if one URL changes, I would have to edit it everywhere in the application.

Describe the solution you'd like

So I want an UrlGeneratorService where you can generate a URL for a specific controller with method and params. Without that it's for example impossible to develop a RestAPI. Or for example if you have a controller method for creating users: UserController.createUser() After successfully creating the user, I have to send back the URL of that user resource in the location header, so I want something like: urlGenerator.generateUrl(UserController.name, UserController.prototype.getUser.name, { uuid: uuid }) or with named Controller methods where you can give a controller method a name and then route to that name: urlGenerator.generateUrl('getUser', { uuid: uuid })

Or in Rest APIs you often have to send back links to other resource endpoints. I don't want to hardcode that URLs. I want to generate that URLs dynamically.

This is a very basic functionality and every framework has it, so I am really wondering how to generate URLs in NestJS.

Teachability, documentation, adoption, migration strategy

https://symfony.com/doc/current/routing.html#generating-urls-in-controllers

What is the motivation / use case for changing the behavior?

I want that NestJS has the same basic features that other popular frameworks also have.

zetti-caletti avatar Aug 03 '22 14:08 zetti-caletti

looks like that lib is abandoned :disappointed:

Another thing we could do is: provide reliable ways to retrieve the metadata needed for such task, ie.,

  • retrieve all routes for a given controller
  • retrieve all routes for a given controller's method

so the dev doesn't need to use Reflect.metadata and so on.

Maybe under the DiscoverModule or RouterModule

https://github.com/nestjs/nest/blob/721372f0c6b1e64a2c9a6a3dbc5ef5e90fa5bd40/packages/core/discovery/discovery-module.ts#L8-L12

micalevisk avatar Aug 03 '22 22:08 micalevisk

This feature should return a url like

/getUsers?randomParam=randomValue

Or return the app base url too? Because if needs the base url I think can be more complex because u can change the url on infrastructure 🤔 Maybe have a optional param called baseUrl?

wesleymatosdev avatar Aug 05 '22 22:08 wesleymatosdev

Another question @zetti-caletti, u are thinking about something under the RouterModule for example or something more specific?

wesleymatosdev avatar Aug 05 '22 23:08 wesleymatosdev

This feature should return a url like

/getUsers?randomParam=randomValue

Or return the app base url too? Because if needs the base url I think can be more complex because u can change the url on infrastructure thinking Maybe have a optional param called baseUrl?

Yes, but you should also have the opportunity to replace the slugs in the URLs. So e. g. /users/{uuid}/?sort=xxx Yes I think the best would be under the RouterModule. Hold on, I'm making a PR and then we can discuss or make further changes.

zetti-caletti avatar Aug 23 '22 15:08 zetti-caletti

@micalevisk @ologbonowiwi @kamilmysliwiec I forked the project, created a branch and added the feature with my ideas. It was hard but I liked it because I never saw the internals of nestjs. I created a separate UrlGeneratorModule and you can configure the absoluteUrl there. I added a RoutingTable service where the routes are stored during bootstrap and then can be fetched from within the UrlGenerator. Didn't knew how to get these routes otherwise. I also added a sample. These are just thoughts, so I didn't added tests. Please review and then we can discuss. https://github.com/zetti-caletti/nest/tree/feature/url-generator

Thanks

zetti-caletti avatar Aug 26 '22 02:08 zetti-caletti

feel free to make a draft PR

would be easier to discuss, I guess

micalevisk avatar Aug 26 '22 02:08 micalevisk

If you need a routing table, you could just extend the existing HTTP adapter (be it ExpressAdapter or FastifyAdapter), override HTTP-related methods (get/post/patch/etc) and just put routes alongside their handlers into a collection (map?).

kamilmysliwiec avatar Sep 19 '22 13:09 kamilmysliwiec

Ok, how to extend the HTTP adapter? But why don't you make this part of the framework? If YOU have to write for example a rest api, how would you generate the links without hardcoding them? I think every framework has tools to generate the urls for controller methods except NestJS? Another example:

return {
    data: post,
    links: {
        self: {
            href: https://www.example.com/api/v2/posts/${post.uuid}
        }
    }
}

zetti-caletti avatar Oct 09 '22 23:10 zetti-caletti

as the http adapter is a concrete JS class (namely ExpressAdapter and FastifyAdapter). You could extend it like any other class and pass it to the NestFactory.create

I like the ideia of having such feature into the fw but, to me, the proposed solution looks to unflexible and opinative, and a bit hard to follow.

I'm thinking on making something with a simple API that still could be used as a building block of something fancy that the end dev could implement...

micalevisk avatar Oct 10 '22 00:10 micalevisk

I was thinking of something like this:

@Controller({
  path: 'posts',
  version: '2'
})
export class AppController {
  @Get()
  fetchPost(
    @Param('post_id') postId: string
  ) { ... }
}
// This will generate:
// http://localhost:3000/api/v2/posts/123
this.urlGeneratorService.generateUrl({
  controller: AppController,
  controllerMethod: 'fetchPost',
  version: '2', // optional. But raise an exception at runtime if not supplied if there's no NEUTRAL version.
  params: { // optional as there's no way to know if `AppController#fetchPost` has `@Param` at build time.
    'post_id': 123,
    // Should raise an exception if `post_id` is not supplied as we know it must exists at runtime.
  },
})

and everything must be strongly typed as much as we could.

micalevisk avatar Oct 10 '22 00:10 micalevisk

as the http adapter is a concrete JS class (namely ExpressAdapter and FastifyAdapter). You could extend it like any other class and pass it to the NestFactory.create

I like the ideia of having such feature into the fw but, to me, the proposed solution looks to unflexible and opinative, and a bit hard to follow.

I'm thinking on making something with a simple API that still could be used as a building block of something fancy that the end dev could implement...

I am open for new proposals, but why looks my proposal too unflexible and hard to follow? It's one method call: urlGenerator.generateUrl('getPost', { uuid: uuid })

The difference is only that in your proposal you give the controller and controller method as a param and my mine I give the method a name and get the URL by route name?!

Undependent how the implementation is done in the end, this frameworks needs definitely such a feature. And again, this framework has over 1 Million downloads per week. I am wondering how all this users build their URLs?! I mean it isn't only for REST APIs. If you want for example render a html view and embed there a link, you also have to generate the URL for that link. But how does all the people do this? Hardcoding all links? I'm surprised that I am the only one who needs this or am I missing something? :D

@micalevisk You can also do a PR, maybe your solutions gets accepted and integrated in the framework :)

zetti-caletti avatar Oct 10 '22 00:10 zetti-caletti

You can also do a PR, maybe your solutions gets accepted and integrated in the framework :)

Sure! I didn't have time to try that out it yet. And I'm not even sure if my requirements are easy of feasible to have.

micalevisk avatar Oct 10 '22 00:10 micalevisk

You can also do a PR, maybe your solutions gets accepted and integrated in the framework :)

Sure! I didn't have time to try that out it yet. And I'm not even sure if my requirements are easy of feasible to have.

What are your requirements? More than mine?

zetti-caletti avatar Oct 10 '22 00:10 zetti-caletti

unless I'm missing some edge case, my requirements for such are pretty much in that sample snippet I've made:

interface GenerateUrlOpts {
  controller: /** Something */,
  controllerMethod: /** an union with all methods names available on `controller` */,
  version?: VersionValue,
  params?: Record<string, any>,
}
interface UrlGeneratorService { // New built-in service
  generateUrl(otps: GenerateUrlOpts): string;
}

It could be called UrlGeneratorService but this name isn't that good because it looks like we could generate an URL from anything

micalevisk avatar Oct 10 '22 00:10 micalevisk

The method could also accept both solutions: whether controller and method or route name. But in my opinion the solution with route names has the advantage that you don't have to remember the controller and method names: urlGenerator.generateUrlByRouteName('getPosts') => /api/v2/posts/ urlGenerator.generateUrlByRouteName('getPost', { uuid }) => /api/v2/posts/123 urlGenerator.generateUrlByRouteName('getPostMedia') => /api/v2/posts/123/media urlGenerator.generateUrlByRouteName('getPostMedium', { uuid }) => /api/v2/posts/123/media/456 urlGenerator.generateUrlByRouteName('getPostTags') => /api/v2/posts/123/tags urlGenerator.generateUrlByRouteName('getPostTag', { uuid }) => /api/v2/posts/123/tags/456

zetti-caletti avatar Oct 10 '22 00:10 zetti-caletti

unless I'm missing some edge case, my requirements for such are pretty much in that sample snippet I've made:

interface GenerateUrlOpts {
  controller: Type<any>,
  controllerMethod: MethodsNames<Opts['controller']>,
  version?: VersionValue,
  params?: Record<string, any>,
}
interface UrlGeneratorService { // New built-in service
  generateUrl(otps: GenerateUrlOpts): string;
}

It could be called UrlGeneratorService but this name isn't that good because it looks like we could generate an URL from anything

So yeah, I think we have the same requirement (independent how its implemented). But really, I would also introduce named routes. Many frameworks has this feature and it makes sense :)

zetti-caletti avatar Oct 10 '22 00:10 zetti-caletti

unless I'm missing some edge case, my requirements for such are pretty much in that sample snippet I've made:

interface GenerateUrlOpts {
  controller: /** Something */,
  controllerMethod: /** an union with all methods names available on `controller` */,
  version?: VersionValue,
  params?: Record<string, any>,
}
interface UrlGeneratorService { // New built-in service
  generateUrl(otps: GenerateUrlOpts): string;
}

It could be called UrlGeneratorService but this name isn't that good because it looks like we could generate an URL from anything

I would also introduce an absolute param to generate absolute urls. So within the UrlGeneratorModule or whatever you should have the opportunity to give a hostname. Look in my Draft-PR, I made a UrlGeneratorModule to configure the absoluteUrl.

And also introduce a slugs param to replace the slugs, because slug = :uuid and params are QueryParams in my understanding.

So you have to take care of hostname/absoluteUrl + globalPrevix + modulePrevix + controller path + method path. I think that's it, if I didn't missed something.

zetti-caletti avatar Oct 10 '22 00:10 zetti-caletti

in my proposal, you could only generate URLs if your app has the access to the controller. So what's the use case of using the hostname other than the one defined when you call app.listen? I didn't follow :thinking:

micalevisk avatar Oct 10 '22 01:10 micalevisk

in my proposal, you could only generate URLs if your app has the access to the controller. So what's the use case of using the hostname other than the one defined when you call app.listen? I didn't follow thinking

Oh, sure you are right. Didn't knew that you can pass a hostname as a second param to app.listen. My mistake -.-

zetti-caletti avatar Oct 10 '22 01:10 zetti-caletti

IIRC most url generators have a separate param to generate absolute vs relative urls. If one is generating urls for cli apps or sending emails, then one probably want to send the absolute url at least in those cases.

I'm not seeing mention of route aliases. That'd be something nice to have. The decorator would take a a second argument for aliases like:

@Get('/signup', 'user_signup')
signUp() {}
generateUrl('user_signup')

with that being case, you don't have to worry about the specific controller method naming, but you would have to error out if there were conflicting aliases.

I think everyone should look at the linked symfony documentation, since it covers lots of useful cases.

ghost avatar Oct 10 '22 01:10 ghost

IIRC most url generators have a separate param to generate absolute vs relative urls. If one is generating urls for cli apps or sending emails, then one probably want to send the absolute url at least in those cases.

I'm not seeing mention of route aliases. That'd be something nice to have. The decorator would take a a second argument for aliases like:

@Get('/signup', 'user_signup')
signUp() {}
generateUrl('user_signup')

with that being case, you don't have to worry about the specific controller method naming, but you would have to error out if there were conflicting aliases.

I think everyone should look at the linked symfony documentation, since it covers lots of useful cases.

Dude did you read my comments? :D I also noticed that, but I called it route name instead of route alias. urlGenerator.generateUrlByRouteName('getPostMedium', { uuid }) => /api/v2/posts/123/media/456 'getPostMedium' is the route name or route alias

Example

zetti-caletti avatar Oct 10 '22 01:10 zetti-caletti

If we change the route, our app will break at runtime. And there's no way to make it type-safe. While using method's name it will break at build time.

But yeah, my proposal has a lack on route alias. I'm not sure how to cover that case yet. I'm was thinking on having a building block to make it feasible to do instead of a full-fledged error-prone solution.

micalevisk avatar Oct 10 '22 02:10 micalevisk

@jrobeson

The decorator would take a a second argument for aliases like:

but route aliases are already implemented:
@Get(['/signup', 'user_signup'])
I don't think we need to change the API of those decorators.

micalevisk avatar Oct 10 '22 02:10 micalevisk

If we change the route, our app will break at runtime. And there's no way to make it type-safe. While using method's name it will break at build time.

But yeah, my proposal has a lack on route alias. I'm not sure how to cover that case yet. I'm was thinking on having a building block to make it feasible to do instead of a full-fledged error-prone solution.

What do you mean with "If we change the route, our app will break at runtime"?

zetti-caletti avatar Oct 10 '22 02:10 zetti-caletti

Also, please, if you guys want to let us know about some feature of that another framework of another language, just bring us the TypeScript version of it instead. It will help us to grasp the desired APIs.

micalevisk avatar Oct 10 '22 02:10 micalevisk

If we change the route, our app will break at runtime. And there's no way to make it type-safe. While using method's name it will break at build time. But yeah, my proposal has a lack on route alias. I'm not sure how to cover that case yet. I'm was thinking on having a building block to make it feasible to do instead of a full-fledged error-prone solution.

What do you mean if we change the route?

@jrobeson

The decorator would take a a second argument for aliases like:

but route aliases are already implemented: @Get(['/signup', 'user_signup']) I don't think we need to change the API of those decorators.

Didn't knew that route aliases already exists. Always though that if you pass multiple paths then you can access the method by both paths?! Or whats the use case of multiple paths like "@Get(['/signup', 'user_signup'])"?

zetti-caletti avatar Oct 10 '22 02:10 zetti-caletti

What do you mean if we change the route?

let's say your app has this:

@Get('/signup')
signUp() {}

and you have some part of your code that generates an URL from GET /signup route.

Later on you want to you change that to

@Get('/register')
signUp() {}

Now that other part is broken because you don't have the GET /signup route anymore.

That's why I didn't like that version. Also, I believe it will be harder to implement on our side.

micalevisk avatar Oct 10 '22 02:10 micalevisk

Always though that if you pass multiple paths then you can access the method by both paths

wait, isn't that called route aliases? my bad.

micalevisk avatar Oct 10 '22 02:10 micalevisk

What do you mean if we change the route?

let's say your app has this:

@Get('/signup')
signUp() {}

and you have some part of your code that generates an URL from GET /signup route.

Later on you want to you change that to

@Get('/register')
signUp() {}

Now that other part is broken because you don't have the GET /signup route anymore.

That's why I didn't like that version. Also, I believe it will be harder to implement on our side.

Sure, if you change the route path then its not accessible on the old path anymore. Thats one of the advantages of the UrlGenerator, that you don't have to care about the route path. Or didn't I understood you?

If you have @Get('/signup', 'signup') signUp() {} and you change it to @Get('/register', 'signup') signUp() {} then the URL doesn't care you, because the route name is still the same. Or what?^^

zetti-caletti avatar Oct 10 '22 02:10 zetti-caletti

If we change the route, our app will break at runtime. And there's no way to make it type-safe. While using method's name it will break at build time. But yeah, my proposal has a lack on route alias. I'm not sure how to cover that case yet. I'm was thinking on having a building block to make it feasible to do instead of a full-fledged error-prone solution.

What do you mean if we change the route?

@jrobeson

The decorator would take a a second argument for aliases like:

but route aliases are already implemented: @Get(['/signup', 'user_signup']) I don't think we need to change the API of those decorators.

Didn't knew that route aliases already exists. Always though that if you pass multiple paths then you can access the method by both paths?! Or whats the use case of multiple paths like "@get(['/signup', 'user_signup'])"?

good to know :)

ghost avatar Oct 10 '22 02:10 ghost