Better HTTP server routing in Go 1.22
eli.thegreenplace.net
eli.thegreenplace.net
Are there go-specific reasons for that? The edge case of "you might register HTTP routes in a bunch of places and it's harder to find that if multiple routes match" seems like something that be worked around with tooling.
I've (ab)used the behavior of "first to be matched wins" a ton in my career - there's been a bunch of "business case" times when I've needed like `/foo/bar` to be one registered route, and `/foo/{id}` to be another.
From the design proposal[1]:
> Using specificity for matching is easy to describe and
> preserves the order-independence of the original ServeMux
> patterns. But it can be hard to see at a glance which of
> two patterns is the more specific, or why two patterns
> conflict. For that reason, the panic messages that are
> generated when conflicting patterns are registered will
> demonstrate the conflict by providing example paths, as in
> the previous paragraph.
See also the background discussion[2]: > The semantics of the mux do not depend on the order of the
> Handle or HandleFunc calls. Being order-independent makes
> it not matter what order packages are initialized (for
> init-time registrations) and allows easier refactoring of
> code. This is why the tie breakers are not based on
> registration order and why duplicate registrations panic
> (only order could possibly distinguish them). It remains a
> key design goal to avoid any semantics that depend on
> registration order.
[1]: https://github.com/golang/go/issues/61410
[2]: https://github.com/golang/go/discussions/60227I'm used to a router where you define a big list of routes all in one place, maybe in some DSL. That's very easy to refactor, and doesn't have any ambiguity. If a library wants a route registered it puts a snippet in the docs for users to copy.
Go in general seems to value clarity over magic at the expense of verbosity, and this is a puzzling exception.
Large code bases are large, and not every program is your simple crud app with a handful of endpoints that can be meaningfully managed a single function.
To me, that's reasonable behavior and is consistent with other things such as https://pkg.go.dev/regexp#MustCompile
People's willingness to defend any and all of go's dubious decisions is really baffling.
I think this pattern in the standard library is a mistake.
import _ "thing"
Vs: import "thing"
thing.Register()
But one uses a strange construct to save a single line, loses the ability to control order, and encourages people to use globals that they can't control.Still, I can understand it for some components like loggers to not add boiler plate to every library. However, I was very uneasy to see that enabling gzip decompression in a gRPC server is done through a magic _ import. You have to initialize the server anyways, so why not just make it an explicit function argument?
In general, the reason you use a compiled / typed language like golang at all (instead of, say, perl) is to "left shift" your bugs: A bug caught when you first spin up your application is better than a bug caught after a corner case acts up in the wild, and a bug caught when you compile is better than a bug caught when you first spin up your application.
I recently ran a cross a bug in a side project of mine (using gorilla/mux) where I had accidentally made overlapping routes. If the router had panic'ed instead of running, I would have been nudged to refactor the URLs and completely avoided the bug.
But that's impractical, so the next soonest time is at startup.
A counter- and/or example of this is what Service Weaver does to try to bomb builds when generated files are older than their dependencies.
For example, Phoenix Verified Routes in Elixir: https://hexdocs.pm/phoenix/Phoenix.VerifiedRoutes.html
Rust can very likely do it by leveraging their `build.rs` stuff to parse and validate call sites of the registration and parameters.
Zig can probably do it with their comptime stuff.
In theory, Go could do the same (but that would mean special-casing the `net/http` handler registration in the compiler). At least `go vet` is smart enough to yell at you about wrong format string arguments.
I'd imagine something like this should be possible as well, but I'm not sure it would be worth it, considering the effort it would take to implement.
[1]: https://twitter.com/anuraghazru/status/1511776290487279616
Template type strings with inference would also allow you to parse the strings in the type system.
I cannot imagine wanting to build my routes table implicitly through import graph rather than having a specific place to aggregate.
Remember C++ actually implements checks at compile time for the modern std::format function. That is, if you mess up the text of a format string so that it's invalid, C++ gives you a compile time error saying nope, that's not a valid format.
You might think that's just compiler magic, as it is for say printf-style formats in C, but nope, works for custom formats too, it's (extremely hairy) compile time executed C++.
<< bug
Am I doing it right?
The panic is the tooling. It ensures that you can’t write code that becomes ambiguous and hard to reason about, or become dependent on something random like the order code is initialised in, which may change for completely arbitrary reasons (for example renaming a file your taking advantage of Go’s special file level init func).
> I've (ab)used the behavior of "first to be matched wins" a ton in my career - there's been a bunch of "business case" times when I've needed like `/foo/bar` to be one registered route, and `/foo/{id}` to be another.
It’s pretty trivial to write a handler that accepts the base path, and then routes to a different set of handler functions. Then the routing is clear and explicit.
Ultimately if you want the behaviour not in the stdlib, plenty of other libraries exist out there with more functionality.
The Go stdlib, and language in general has always skewed towards conservative behaviour in the face of possible ambiguity. Something I’ve always appreciated, because it means it’s easy build a strong and accurate intuition of how the stdlib works. You rarely find yourself in situations where what you think is the “obvious” answer isn’t the correct answer, simply because your idea of “obvious” doesn’t perfectly align with the authors idea of “obvious”. Being able to quickly and accurately read and understand code is far more valuable than saving a handful of seconds when writing it.
It seems a little pointless to have route handling functionality that requires more route handling for pretty normal cases.
Honestly if you're running into the second case above, I would question the wiseness of whatever it is you're attempting to do, because reasoning about the behaviour is unlikely to be clear and obvious if registration order is the only differentiator.
Looks like it will panic in case you have `/foo/{id}/delete` and `/foo/bar/{action}`. /foo/bar/delete will match both, none is more specific so it panics. Feels reasonable. Having a first one wins precedence might be better though.
HTTP server panicking is never reasonable
If you've managed to ship code that panicked during execution due to path routing ambiguity, then honestly your code is probably so riddled with bugs this is going to be the least of your issues.
You can of course always implement this within `/foo/*`, but then you're implementing your own URL routing and working around the framework rather than working inside the framework.
This feels to me like it's a change designed for APIs, not a change designed for user-facing websites. For APIs URL structure is often well defined in a technical sense, and this sort of use-case is rare. For a user-facing site however there are UX concerns, marketing concerns, SEO concerns, all sorts of reasons why some lovely, technically correct REST-style URL structure just won't work in practice. Unfortunately I know this from experience.
The behaviour documented here is actually extremely sensible and is best practices for anyone calling themselves a software engineer. Fail as early as possible, with as clear a message as possible.
> Longer patterns take precedence over shorter ones, so that if there are handlers registered for both "/images/" and "/images/thumbnails/", the latter handler will be called for paths beginning with "/images/thumbnails/" and the former will receive requests for any other paths in the "/images/" subtree.
This may just be a syntax thing, I was being loose with syntax, and meant that the `*` would match anything for the purposes of this example.
With the panic, I have to write some spaghetti code with a recover in a goroutine.
Many are misunderstanding when the panic happens. It does not happen when the user requests the path, it happens when the path is registered. The user will never arrive at that path to be notified. You will be notified that you have a logic error at the application startup. It can be caught by the simplest of tests before you deploy your application.
Debugging the latter one is much more harder.
There’s `recover` of course, but I see no harm in returning an `error`, especially given that `errors.Join` is a thing now, so one doesn’t need to copy-paste ifs.
The only reason not to is keeping the function signature intact.
And panic/recover is not idiomatic Go here, as it's not an exceptional situation, just an error.
YMMV, of course.
Secondly and most importantly they don't error. Caddy will simply match the last declared rule while Traeffic has a system of priority labels and yet for conflicting rules with same priority again it will match the last one (or even worse a random one). Both of these are very dangerous because you may realise late (from statistic logs) that you are routing the wrong requests to the wrong handler, and have already missed countless calls that should have gone on the first declared one.
Even if the framework could guarantee the order of registration, this source-order heuristic is easy when you work alone or in a small team. Good luck guaranteeing order of routes in a large project with many teams.
Actually forcing you to not depend on things that can cause hidden bugs is a good design decision.
I have also ran into situations where I needed a routing tree like
[["/foo/bar"]
["/foo/:id"]]
but it's better IMO for routing libraries to force the user to acknowledge they are introducing conflicting routes rather than silently resolve conflicts. That way the user is forced to understand the behavior of the router.Given opposite expectations, erroring out makes sense, but a panic? Does that mean it crashes the whole web server when a client first accesses it, when you launch the server, or does it return a 500 to the client?
But its nice that this functionality will be provided by Golang itself.
[0] https://gorilla.github.io/blog/2023-07-17-project-status-upd...
[1] https://www.reddit.com/r/golang/comments/1528e25/gorilla_web...
1. I want to send a PR!
2. Project doesn't seem very active, so never mind.
Plus I'm not checking if any of my dependencies need help every day.
I took over fsnotify after it was archived because I just didn't know they needed help. Last guy spent about 5 years looking for someone to take it over. I wouldn't have minded doing it before, but ... you do need to know about it.
It's kind of an annoying project to work on because everything is platform-dependent (and at times fickle) so you can't "just" run "go test ./..." but really do need to test it on all platforms. BSD and illumos aren't too much trouble, macOS is already rather painful (slow), and the experience of running Windows is not something I'm able to describe using only polite terms.
Libraries... yeah, too much work to monitor those, especially having in mind that the chances of a change affecting you negatively are likely in the fractions of a percent.
You need to weirdly create a magic string to define your handler. Why not make it an actual argument, then using already existing constants is easier.
[1] https://github.com/golang/go/issues/61410#issuecomment-16580...
Adding new arguments to the Mux interface would break existing code, given the method signature can’t be changed, these magic strings seem like a reasonable compromise. It not like HTTP verbs are going to change anytime soon, and it’s trivial to validate them during register, or via static analysis. So I’m not sure what value the use of constants would bring, especially if it either broke backwards compatibility, or forced the creation of a new, but slightly different mux API that would have to live in parallel with the old API forever.
That wouldn’t break compatibility
Certainly it's technically a breaking change, but there's huge difference between making all existing usage of the mux interface incompatible with the new changes, and only making obviously incorrect and buggy usages of the interface incompatible.
This has always resulted in a panic[0].
[0]: https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/ne...
I don't really see an issue in treating it as if it was just `Request-URI` (in RFC2616 terms) with some parameter magic, and became a Request-Line-resembling `[ Method SP ] Request-URI`, which is fully backwards-compatible and isn't exactly surprising to anyone. I think it's pretty much obvious what it does even if one never sees the documentation.
And given that `Method` is either one of a few predefined constants or `extension-method = token`, and `token` excludes whitespace, slashes and all sort of brackets I don't think there's a chance of confusion or misparsing there, even for weirdest custom methods.
The compiler via a method call or parameter can capture this constraint in a pretty straight forward way. I'm not sure why an API would give that up
With this proposal if you already have an app using mux, you can change one line and you have app using new mux which you can then evolve to take advantage of additional capabilities.
With new name you have to rename all your codebase.
Not to mention that writing a wrapper with an API to your liking is few trivial lines of code so this is bike shedding at its finest.
In my head it's as if we did this:
http.Segments("foo", "bar", http.Param("barID"), "baz")
instead of: /foo/bar/:barID/baz
Is it sliiiightly safer? Yes. Is it insane? Also yes.I sure don't.
And those that do probably aren't careful about HTTP headers either.
So in practice this an improvement for most.
And if you don't like how it works, there are plenty of alternatives to use.
Arguably this is more correct that letting the users declare a separate Handler that can neither guarantee the same headers as the the GET Handler not guarantee that the body is not sent the response.
I'd prefer the type safety of verb-specific methods (i.e. mux.Get, mux.Post etc) than magic strings validated at run time. Additionally editors can autocomplete/intellisense methods.
func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
Obviously skipped the types for brevity.They’re convenient but error-prone. I think everyone who wrote a decent amount of Go had that malformed, misspelled, or misnamed (“db” vs “sql”) tag at some point.
However the core language way of dealing with enums for example is extremely weak. It's common to have a typed enum on a struct. When parsing the struct, random string (or whatever the alias is) values sneak in and the only thing you can do is validate.
Backward compatibility, they didn't wanted to change function signature nor add different method for it.
Apparently, a 405 with a properly-populated Allow header.
https://cs.opensource.google/go/go/+/master:src/net/http/ser...
I know people don't love the stringly-typed interface, but rather than typo the HTTP method name, I suspect I'm more likely to simply type the wrong thing in correctly anyways. So I'd be fine with having static analysis warn about bad syntax personally.
For what it's worth, though, in my opinion, you probably shouldn't use the default serve mux if you already have advanced needs; there are plenty of options out there that are more suitable to different use cases, and if you're already doing dynamic route generation, it may wind up being less effort to just write your own router rather than try to munge whatever data structures you have into an existing router's.
In their example here, had they defined the `/task/0/{action}/` path before the wildcard path, my expectation would have been for that to match first. This would allow for handlers for special cases to easily be defined.
I’d far rather things just do what I say than fail in “helpful” ways. Smells really funny and not very Go.
The original ServeMux was designed to not honor registration order, because it is risky to do so. One of the key design goals of Go is to support "programming at scale", and odd side-effects due to edits made to "distant code" is the exact kind of thing you want to avoid.
In trivial examples, where all of the registrations are made in a single function in a single package, there is clarity around the intention of registration order. But you can't assume that that is how these things will always happen. In fact, for any sufficiently large codebase, these registrations will likely be happening in more than one location, and may be the result of reading input files or generated code where the person writing the spec for the generated code is unaware of the intricacies of execution order and how that may impact routing. It also means that changing the lexical sort of a set of package imports could result in an unexpected change in routing.
Avoid unexpected results, making refactoring easier, and generally trying to make a complex program easier to reason about, are definitely very Go. You might not always agree with their choices, but those are their reasons and they are remarkably consistent.
Of course it's too late now but I'm surprised this API was ever considered because it seems obviously scary and wrong to let a dependency just create it's own routes.
Rather than have a single global mux have a mux instance. You call methods on that instance to register routes and then serve the instance. This means you can use your ide to find all routes.
I’ve gotta take a look at htmx.
I was hoping that bud with svelte compilation would fill this gap, but looks like templ is a nicer alternative.
> Make a dedicated method that accepts http request method names
What problem does that solve compared to embedding the method in the string?
The problem it solves is not having string typed stuff which is a consistent pain point where it happens.
router.GET("/", func(context *gin.Context) {
...
}
And it seems to be no different than many other languages/frameworks. Are there more examples that use the "GET /path/" way ?It does sound like a micro-optimization.
For example:
(Http.GET, "/path")
Rather than the current: ("GET /path")
Odd.But I'd prefer just a bunch of mux.GET/mux.POST/etc functions instead of that.
Agreed on the dedicated GET/POST etc functions though.
So `mux.HandleFunc("/"...)` always handles _everything_.
And there is no easy way to say "no, just handle exact matches, plus maybe ? queries". As gorilla/mux does.
I don't think that is changed in the new go?
"There is one last, special wildcard: {$} matches only the end of the URL, allowing writing a pattern that ends in slash but does not match all extensions of that path. For example, the pattern /{$} matches the root page / but (unlike the pattern / today) does not match a request for /anythingelse."
That is amazing. Goodbye gorilla mux.
But that’s not backwards-compatible, sadly, so this is going to remain a historical wart.
> Longer patterns take precedence over shorter ones,
[edit] Heh, I guess you'd know better. :D The comparison between the hn username and github username resolved favourably in the end. :P
The primary utility of Go's net/http is that it is a "minimal framework" that provides a fairly common plug-level compatibility between various bits and pieces. The particular bits that it happens to provide by "default" are not really that consequential by comparison. I was actually surprised anyone touched the standard mux at all at this late date because there's so many other options already, and most of them just plug in with no fuss at all. All a router is is a handler that examines the request and then calls another handler as a result.
[1]: https://github.com/julienschmidt/httprouter
[2]: https://github.com/infogulch/pathmatcher
[3]: https://github.com/julienschmidt/go-http-routing-benchmark
This is so backwards it's not even funny. It is supposed to be exactly the other way - conrete paths MUST TAKE PRECEDENCE over patterns.
they say that get /foo/{id} takes precedences over path get /foo/23 or get /foo/bar
And indeed they do.
This means that the general perception "these projects are production-quality but stdlib isn't" is misleading. If I have to choose web framework or library that implements feature X incorrectly versus one that doesn't have X at all and I have to write it by myself, I will with no doubt choose the latter.
The string typing thing.. I can understand why they did it even if it is not the thing I would have chosen.
app.handleGet("/route/goes/here", (req, res) => {
});
Which seems like it's useful for making really quick and dirty micro-services (nano-services, even), but I still vastly prefer the more declarative and modular schemes of bootstrap or asp.net: // Middleware automatically routes "/Foo" to FooController
[Controller]
public class FooController : Controller
{
private IMyService _service { get; init; }
// Declarative dependency injection
FooController(IMyService service)
{
this._service = service;
}
// HTTP Method and Route are declarative
[HttpGet("/")]
public ActionResult GetBar([FromBody] Model myModel)
// ^ automatic user input validation using reflection
{
this._service.create(myModel); // or whatever you need to do
return Ok();
}
}
This has always seemed way more maintainable to me. Even microsoft has added the ability to do these quick and dirty HTTP routing methods along with top level statements.Many people (myself included) hate decorators. Keep my code declarative and free of black magic, please. This pattern also conflates class structure with route structure. What if I wanted to assign a method in this class to another base route? You end up with routing strewn all over the application.
Sure there is. It's metaprogramming. Anyone in the world who can read code can understand this immediately:
app.get('/', (req, res) => {
res.send('hello world')
})
Throw decorators in the mix, and now I need to learn exactly what this specific environment is doing with those annotations. I have absolutely no way of understanding it at a glance. And you're now also stuck with vendor lockin to whatever framework/compiler was using them, and your code can no longer be fully isolated and unit tested without the framework.> And you're now also stuck with vendor lockin to whatever framework/compiler was using them
I don't understand. If you choose a web framework, you are locked in to developing things in that framework from now on. In what world do companies try to change web frameworks without having to change any of the underlying code? In what world would they want to do so?
And he does have a point about vendor lock-in. I tend to classify these kinds of frameworks as "cancerous" - as they metastasize and define how you can express yourself, and paint you into a corner where it gets really hard to rid your codebase of the framework should that be necessary.
Part of my job in the past has to be technical due dil for M&A. This kind of design approach usually results in a red flag if a major part of the valuation is the codebase.
Are you in the habit of hiring people without experience? Developers had to spend months (years) learning javascript before they learned any frameworks. Would you rather switch to point and click programming so that your developers don't need to actually learn to code?
> It means that the code is readable only to those who have spent a lot of time working with that framework
Knowing the framework (or being able to learn) that the business is based on should be a requirement for working there. You should not hire people who are incapable of learning things, or who can only do things in one particular way
> And he does have a point about vendor lock-in. I tend to classify these kinds of frameworks as "cancerous" - as they metastasize and define how you can express yourself, and paint you into a corner where it gets really hard to rid your codebase of the framework should that be necessary.
All frameworks will place limitations on how you express yourself. Compared to javascript, C# (and .net) offer far more flexibility and metaprogramming abilities. Try declaratively validating user input in Javascript or Typescript without having to rely on some kind of runtime hack or re-writing the same code over and over.
If you were to decide to ditch express and move to a different framework, then the way you have written your express handlers would also have to be totally discarded. By choosing any language or framework, you are tying yourself to the technology decision and labor pool associated
> Part of my job in the past has to be technical due dil for M&A. This kind of design approach usually results in a red flag if a major part of the valuation is the codebase.
I question your judgment if using a well documented and not at all obscure framework based on some of the most popular frameworks out there (MVC style, bootstrap, etc.) raises a red flag. It would indicate, to me, your lack of experience in writing or reading code rather than anything about the framework itself. If having a javascript backend isn't a red flag in itself to you, then I would pretty much just discard any feedback you would have about a web backend
I hire people who I believe can produce quality code as part of a team. I've hired people with zero experience and with 35+ years of experience. I've probably hired somewhere around 200 people. I have no idea how many people I've interviewed.
I have both hired people with decades of experience who turned out to be poor hires, and I've hired people without any experience who went on to make critical contributions to billion dollar projects.
If your hiring criteria are "has experience with X" you are limiting the size of your hiring pool to people who are heavily invested in "X". That is probably not the most brilliant hiring strategy. For one it means you can never hire people who have newly graduated.
> All frameworks will place limitations on how you express yourself.
True, but some frameworks will more severely limit your future options and be harder to move away from. The more of you application is affected by the framework, the more expensive it is to move away from it. This is an important reason why the Go community tends to discourage creation and use of large frameworks.
I can understand that you are defensive if you have spent years making this investment and someone suggests you have made a poor choice. But I think it would be time well spent to try to understand why many companies are moving away from the "big framework" approach.
> I question your judgment [...]
That's fine.
> It would indicate, to me, your lack of experience in writing or reading code [...]
You're free to make that assumption. Even though it makes you look a bit silly since you are making assumptions about something you lack data on.
Not necessarily. I wouldn't hire a new grad without an internship or without even some personal project experience. If I were to hire someone out of college, I would expect them to have made a website at some point before and to be able to explain to me how it works. I would also expect them to be able to make the same website in different frameworks if asked to change their tech stack.
> I can understand that you are defensive if you have spent years making this investment and someone suggests you have made a poor choice. But I think it would be time well spent to try to understand why many companies are moving away from the "big framework" approach.
It's quite to opposite: I would expect anybody working with me to be flexible enough to learn different ways of expressing themselves in code, and I would expect them to not take several months to learn something as simple as a web framework.
My complaint is that I don't want things in my code affecting my code that aren't code. Most web frameworks avoid this, while the more enterprise stuff like Spring, Dotnet, et. al seem to lean into it. It's kind of the same argument as SQL stored procs. Should you rely on embedding your business logic directly into the runtime? Probably not.
Ultimately it's just personal preference. But if I can't compile something in my head at a glance, it shouldn't be a part of the codebase IMO.
I don't know if that's true, but I doubt it.
> My complaint is that I don't want things in my code affecting my code that aren't code
Many things affect your code under the hood that you don't see. There is no difference between using a decorator/attribute and having a config.json file for other things.
> while the more enterprise stuff like Spring, Dotnet, et. al seem to lean into it.
Because they are responsible for larger services which need to be more maintainable and stable.
> But if I can't compile something in my head at a glance, it shouldn't be a part of the codebase IMO.
This would preclude ever using a programming language or framework which you don't already know. You have been taught to write web services in one particular way; the fact that you don't know other ways doesn't make them "unreadable", it just means you don't know how to read it
I don't know why, but I truly do not like this. So if I create a "HelloController" class does the middleware just start automatically routing "/hello" to it?
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
https://learn.microsoft.com/en-us/aspnet/core/mvc/controller...If you don't call one of the MapController methods, requests will not be routed to controllers even if they exist in the same project.
The second example has poor readability. And that's even before we get to the issue that this approach has fallen out of favor exactly because it result in code that can be a pain in the neck to figure out.
Please don't do this. This is the kind of legacy approach that I try to teach people working for me NOT to follow.
Only if you don't know C# or asp.net (or bootstrap). In which case: why would you be working for an organization which does?
> And that's even before we get to the issue that this approach has fallen out of favor exactly because it result in code that can be a pain in the neck to figure out.
This kind of structured approach has fallen out of favor because it's become more popular to hire javascript and react developers straight out of boot camp who have only been taught how to code one or two things.
The argument that code needs to be inherently readable to someone with 3 weeks of experience is how you get hundreds of poorly written spaghetti code microservices written in node or typescript and not a language better suited for backend development. If you want your code base to constantly look like it was written by someone following their first javascript tutorial, then by all means continue using this top-level express JS crap
> Please don't do this. This is the kind of legacy approach that I try to teach people working for me NOT to follow.
You are setting yourself up for some bad tech debt in the future
You've never taken a job working in a language you don't know yet? Anecdotally this is a common thing. In fact, my most recent job hired me to write C# with nothing but prior Go/Python experience.
> This kind of structured approach has fallen out of favor because it's become more popular to hire javascript and react developers straight out of boot camp who have only been taught how to code one or two things.
This comes across to me as needlessly bitter to folks with less experience than you.
So then it shouldn't be a problem for you to learn a new skill based on a very common pattern with many examples in different languages
> This comes across to me as needlessly bitter to folks with less experience than you.
If those folks are forcing the business to make tech decisions based on their lack of experience, it makes life harder for others. If you are going to choose between "should we write our backend in C# or javascript", and the decision comes down to "well our bootcamp grads don't know C# and they don't have a background in software engineering, so getting them up to speed on C# will take months", then you're willingly choosing inferior tech to make up for subpar employees
Perhaps you should be open to the possibility that you're wrong in assuming that? Perhaps there are people who dislike these kinds of designs precisely because they have experience with them?
(hint: I wrote my first IoC container/framework about 20 years ago)
You are wrong in your perception
IMO it makes it much easier to get an overview of the overall functionality of the app, and to find the code which implements each route. It's also a lot more flexible if you ever need to support routes which do not fit the conventional pattern of the framework (perhaps for legacy reasons).
You can of course still use dependency injection, etc with this central route registration model.
So what you are saying is that the IoC / Decorator approach is the way to avoid spaghetti code? That's an interesting assertion.
> You are setting yourself up for some bad tech debt in the future
You mean like being saddled with a product that depends on a large framework that fewer and fewer people want to deal with?
The way to avoid tech debt is to retain plasticity, keep more options open and not paint yourself into a corner where your choice of framework dictates how you structure entire systems.
And, of course, to hire people based on talent and ability rather than a line on their CV that promises experience with a given framework.
You're also doing more in your second example. You can still do service injection in JavaScript.
1. Uses declarative routing, which looks better and makes more sense than running a function to handle routing (e.g, you read it as "There is a handler named GetFoo living within the Foo controller that exists at the base route"). Declaration is more important to the end user than implementation when creating an interface. It also allows you to export these classes to a different Main method which can generate documentation for you without having to even run the HTTP server (the declarations can be picked up using RTTI and not actually having the runtime server activated). You can also add named parameters to the routes and then add them to the body of the handler with their proper types, e.g:
[HttpGet("/{id}")]
public ActionResult GetFoo(int id)
And that input validation is handled automatically. Named parameters exist on a lot of these frameworks as well, but Javascript doesn't provide static typing so it's not a good language for writing a backend in2. Handles input validation under the hood or through middleware so you don't have to manage it in the controller body
3. Can also handle user auth under the hood using attributes so you don't have to handle it in the controller body
4. Uses middleware to handle routing in a predictable way (e.g: the name of the controller is the name of the route by default)
5. Uses class-based controllers to encapsulate the services available to the controller through the constructor, and also helps pair a single model to a single controller for making really simple RESTful interfaces
You can also define a custom controller interface or abstract class which provides a bunch of default functionality that you commonly re-use (e.g: ApiController with some built in response wrappers/handlers for error codes, etc.)
God the number of bugs and bad practices I've had to deal with and work around do to auto-magic casting of url segments, or JSON blobs into language native types, is too damn high. Auto input validation based on function parameter types sounds good, until you think about for more than a couple of seconds and start to realise type primitives definitions vary significantly between languages. Using them for input validation just means exporting your languages type primitives into your API, with zero consideration of if that's a good idea.
The classic example of this is people using the `int` type for `id`s just because the IDs happen to contain only digits. Ignoring the fact that identifiers aren't numbers (performing numeric operations on them doesn't make any sense), but because you've now blindly exported the `int` type into your API, you also export nastiness like precision (try preserving leading zeros with your `int` type) and rollovers into your API. Two things that make zero sense when dealing with identifiers, and two things that often change depending on the exact platform your software is running on.
I'd really encourage you to explore other languages. If you're one of those "JavaScript is bad" people, there's plenty of others: Go, Ruby, Python... I'm surprised to be seeing this kind of comment on Hacker News to be honest.
I know that angular is really good at this, but I'm not sure what pure js frameworks would allow a service to be defined as an interface first, and then injected into the constructor of a controller (or even a handler function) based on the implementation method selected somewhere else. I just haven't seen it happen.
app.MapGet("/", async (
[FromBody] User user,
[FromServices] MyService service) =>
{
await service.Handle(user);
return Results.Ok();
});I also dislike functions and controllers without explicitly defined return types.
Then you add an extensions method for WebApplication that registers all of your route groups when configuring your app pipeline. Each time you create a new route group, you add that group to your WebApplication extension method and it's wired up.
It's a little extra work up-front when you're first creating your app, but it also results in a clearer structure for how routes are defined once the app grows into the size you're describing.
Even .NET is moving towards your first example by providing minimal APIs that allow simple binding of a route to a handler function.
Thankfully it looks like even ASP.NET Core is no longer using this kind of syntax. https://learn.microsoft.com/en-us/aspnet/core/fundamentals/r...