Phoenix API possible improvements and existing annoyances

Mostly just bikeshedding here, but I have a short list of fixes I’d love to see in Phoenix to fix certain annoyances and usability issues. All in an effort for these fix ideas to potentially be ripped apart, and if they survive that then maybe Phoenix can get some enhancements. :slight_smile:

Phoenix.Router

Route Ordering/Ranking

I really don’t like that there is no way to order route paths except by literal order in the Router file, meaning I have these odd one-off scopes scattered all over, adding something like an order or rank or so to the options would make for ease of overriding a lot easier, in essence the ability to do this:

get "/:ah_tag/gradebook", GradebookController, :index, as: :ah_gradebook, rank: 1

Default rank would be 0. What this would do is it wouldn’t be matched at first, rather the entire rank 0 (and less) list would be tested first, then finally rank 1 would be, so this would be ordered after everything else in the route tests. I know this is not a big thing for things without a ton of routes, but when you start exceeding 400 routes and have to put weird one-offs all over the place instead of with related routes, it’s very disconcerting and easy to miss things.

In something like mix phx.routes you could either order then based on the rank (how they are actually executed), or perhaps add something like (1) to the route line to show the ranking number while keeping the related routes together.

Route matching

Right now routes match based on the method type like get or post or so, and the path itself, but if you want to match based on some other data on the connection like if they are logged in or not, then you either need to setup a plug to dispatch manually (very error prone). The general way is to just have a plug that redirects to, say, a login page, and that’s easy to do with plugs, and for this specific case it isn’t hard, but now imagine you have 10 different conditions that all need to intermix and work together, this becomes quite a pain very quickly. In essence I want something like this:

get "/something", MyController, :handler_private, if: LoggedIn, if: {SomethingElse, :fun_plug, [extra_arg]}
get "/something", MyController, :handler_public

And if it wants to return data it could stuff it into the plug, essentially it’s a plug where the call returns an {:ok, plug} or {:error, plug} instead of just always returning a plug. And the reason to return a plug both in ok and error is so that when it is ok it can add data to it for the handler to use, and if it is an error it can still add data to it, perhaps for either caching reasons to not run a potentially complex call again or so. This would be super useful and concise way of having a route only be routed to if certain conditionals exist, while having an easy way to decorate in information based on those conditionals.

Right now to do similar things I either have to check in my action handler and dispatch out to different functions based on conditions and checks, and this involves getting repeated a LOT over the 400+ routes, or you have to create a plug to handle it, and in any case it absolutely consumes the route, so if you want to just ‘fail out’ and let routing continue to other possible route matches, you just can’t, at least I haven’t seen a way to do so.

Route path arguments should not be string keys

Something like:

get "/account/:account_id/:account_section", AccountController, :show_account_section

Will have an %{"account_id" => _, "account_section" => _} passed into the handler, which conflates and mixes path arguments with get/post params, they should be atoms to distinguish themselves and make forgetting which is which in the handler less likely.

Route matching based on types of path arguments

Constantly putting code in near every route to convert path arguments to the correct types conflates the code annoyingly with boilerplate, it would be nice to be able to specify it in the routes themselves, perhaps like:

get "/account/:account_id/:account_section", AccountController, :show_account_section, cast: [
  account_id: {Account, get_account, []},
  account_section: {Account, get_section, []},
]

Where account_id would instead of being a string in the handler would in this case actually be a proper account_id type (struct, integer, whatever) confirmed that it exists in the database, and the account_section would use both the prior already-parsed account_id and the string account_section to lookup the actual section and thus return that to the account_section field passed to the handler.

Could even cast in other things:

get "/account/:account_id/:account_section", AccountController, :show_account_section_admin, cast: [
  admin: {Account, :is_admin, []},
  account_id: {Account, get_account, []},
  account_section: {Account, get_section, []},
]
get "/account/:account_id/:account_section", AccountController, :show_account_section_user, cast: [
  user: {Account, :is_user, []},
  account_id: {Account, get_account, []},
  account_section: {Account, get_section, []},
]
get "/account/:account_id/:account_section", AccountController, :login # Not logged in at all

Maybe cast should be called guard or something instead…

The lack of being able to match on query arguments is more of an issue than I expected

It would be wonderful to be able to specify a route like:

get "/something?some&:thing", MyController, :handler

Which would fail to route to this if both the some (with any value, if any value at all) and the thing (which requires a value). So it would route if it where something like:

  • /something?some&thing=42
  • /something?thing=42&some Reordered is fine
  • /something?thing=42&some=ignored
  • /something?some=more&query=args&thing=42 Unmatched query arguments are ignored for matching purposes.
  • /something?thing&some The thing key has no value so it would get set to nil, if you want it required then use a cast to fail when it is nil or so.

And these would not match:

  • /something?thing=42 Missing some
  • /something?thing&some The thing key has no value

In the dispatch tree this would be efficiently handler by map matching.

You could even make something like this:

get "/something?some&thing=:thing", MyController, :handler

Make it so that thing must have a key and can’t be null perhaps.

You could match the ‘rest’ of an argument by appending something like ... or so to the end of a value, so:

get "/something?:thing&:some...", MyController, :handler

Then the query argument thing would go into the thing field but then some would be a map of all remaining query arguments.

4 Likes