(评论)
(comments)

原始链接: https://news.ycombinator.com/item?id=37898999

总的来说,这次讨论中提出的第二个代码片段的主要批评包括可读性差、处理路由的灵活性不足、潜在的技术债务、扩展到复杂网站的困难、缺乏明确的返回类型以及过于冗长的倾向。一些用户推荐的替代方案是声明式路由、自动输入验证、使用中间件进行输入验证和身份验证,以及基于类的控制器。其他用户评论说,在express.js普及之前,诸如Sinatra和NancyFX(C#)等框架具有类似的语法,但现代替代品如NancyFX或ASP.NET Core正逐渐摆脱这些较旧的语法。然而,用户之间的意见可能有所不同。

相关文章

原文
Hacker News new | past | comments | ask | show | jobs | submit login
Better HTTP server routing in Go 1.22 (thegreenplace.net)
314 points by ingve 2 days ago | hide | past | favorite | 226 comments










Forcing a panic when 2 routes are matched seems counter intuitive to me (versus, idk, every other web framework which uses the first-to-be-registered route that matches).

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.



You can't guarantee the order of registration will always be the same, so it's really undefined behavior. This is how the original ServeMux was designed and implemented, and they felt that was useful behavior to continue to support.

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/60227


This seems like an issue caused by registration at a distance. I'm new to go and it is one of the things that felt most wrong.

I'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.



It’s not an exception. There is no magic.

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.



Hard panic in a "not simple app with large codebase" app because you couldn't match a route is amateur hour.


I think you're assuming that the panic happens when a request is received, but it actually happens when a conflicting route is registered.

To me, that's reasonable behavior and is consistent with other things such as https://pkg.go.dev/regexp#MustCompile



I think you are vastly oversimplifying what has been said or why it works the way that it does. I really recommend you read the design document and reasoning, as it is rather clear why they are doing it the way that they are. If you have a cogent contribution to the discussion, please do share.


Others have already contributed more than enough.

People's willingness to defend any and all of go's dubious decisions is really baffling.



Registering handlers is a startup activity ... Practically the only time I allow a panic in my code. I also have unit tests for the entire startup sequence so theoretically code with ambiguous handlers would never be committed.


Go's flag, log, image, and sql packages all work this way, so it's not just the HTTP router. I sort of see both sides. Having implicit registration makes it very easy to have different teams working on different packages and you just import the package and it registers itself. But it also makes the behavior of the resulting final binary hard to understand and based on implicit code instead of explicit. I personally try to just have one big routes() http.Handler function that returns everything all in one place, but I get why that isn't always practical.


It doesn't feel to me like much of a gain over having said team expose a specific default self registration function. Then that act can be explicit rather than action at a distance on some dependency defined global state.

I think this pattern in the standard library is a mistake.



Yeah, the trade-off is literally:

   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.


I very much dislike import side effects in any language. Im a lot happier where thing.Register() is still forced to happen in the main function.

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?



thing.Register() may register a route that conflicts with yours and it will never be matched in this case if declaration order was taken into account. You may discover this too late when you either have missed important calls on that thing or when requests intended for that thing are causing unintended effects on your first declared route.


That is all also true when doing the same thing in an init func.


Well, yeah, with the "implicit registration" model you can either split it up or do it in a central place, whereas if they would require you to do it in a central place, you wouldn't have that choice. It probably boils down to "library" vs. "framework" - Go's standard library doesn't want (as much as possible) to be a framework that forces you to do things its way, and as far as I'm concerned that's ok...


How is this new to Go? Almost all Java frameworks that rely on Annotations do registration at a distance. Controllers are Routinely declared on different packages and even injected from dependency libraries.


I agree with most of this post. The one exception in my experience: Vertx. I never saw any annotations for routing. It is a very verbose library, but that means there is no black magic.


> Are there go-specific reasons for that?

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.



Wouldn’t the “left-shift” be a compiler error instead of a panic?


That's where the focus usually is, but aborting at startup is the next best thing. It's an undervalued technique.


This particular issue is undecideable at compile-time in the general case, so panicking on registration (ie, preventing the process from starting at all) is the next-most visible and noisy error option.


It can not be decided on compile time but it provides the basis for a build time "error". It's enough to have a testcase that registers your routes and the bug is descovered even before you commit potentially.


Ideally.

But that's impractical, so the next soonest time is at startup.



Go as a language is generally not sophisticated enough to do that sort of thing.

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.



I'm having trouble thinking of a mainstream production language with a stronger type system that could make a compile error out of the string argument passed to an HTTP router; can you think of one? Or of a non-string-typing for routes that solves the same problem, again in something people ordinarily use to deploy to production?


OCaml can definitely do it (for example, you get a compiler error if you pass the wrong arguments to a `printf` where the format string specifies, say a number, but you pass in a string).

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.



> that could make a compile error out of the string argument passed to an HTTP router

For example, Phoenix Verified Routes in Elixir: https://hexdocs.pm/phoenix/Phoenix.VerifiedRoutes.html



Typescript's type system is known to be turing complete and people have implemented things such as sorting/tree-walking just using types (i.e. during compile time) [1].

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



I'm inclined to suggest the typescript could make compile errors of ambiguous routes, though I don't see any obvious reasons way without an explicitly referenced agreggate type for existing routes. So perhaps not if routes are initialised implicitly like this.

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.



Most languages with macro or templating should be able to define this behavior as a library. Rust and C++ come to mind, for example.


Can you provide an example of a Rust HTTP routing library that can generate a compile-time error for overlapping routes?


I don't know of one, but you seemed to doubt not that it's actively being done, but that it's even possible, which is a very different proposition.

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++.



> "left shift" your bugs

Am I doing it right?



ug


> 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.

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’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.

It seems a little pointless to have route handling functionality that requires more route handling for pretty normal cases.



Almost all the examples provided as "pretty normal cases" of having overlapping path registrations are already handled without panicking by the path precedence rules. All the cases where you have a wildcard path + handlers for specific variants of the wildcard values, are handled as expected without panics. Only the rather extreme corner case of having two wildcard paths, with wildcards in different locations, but still matching the same path, results in a panic.

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.



I believe it allows `/foo/bar` and `/foo/{id}` as the first one is more specific and has precedence. This looks fine to me.

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.



The least surprising behavior would be matching `/boo/bar/{action}`, since we're dealing with path element separated by slashes, and the precedence case already exists otherwise.


I would expect and prefer the longer literal prefix to match. If the goal is to not have so many different routers in use, having some options like this case would be better than having one opinionated Go-way, which is how we end up with many libraries to fill common gaps.


Na it’s dumb. Bar is very specific. It should be picked.


It's a 404 Not Found or 500 Server Error.

HTTP server panicking is never reasonable



The panic happens at handler registration i.e. binary startup, not while performing path matching. Having literally any tests in your application, literally anything that executes your binary before you ship it, will tell there's a problem. Your code simply won't be able start serving traffic if there's path routing ambiguity.

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.



Getting an error instead of the incorrect data because the route that was actually ran was another one is pretty good for debugging. I have actually ran into this with Django, one route was missing the ending "$" regex.


I get the frustration, but URL routing correctness is fairly trivially tested in Django and other frameworks to work around this issue, whereas it sounds like the matching algorithm here simply does not support certain common use-cases.


What common use-cases does it not support?


The `/foo/bar` and `/foo/*` use-case, where you want the first to go to a special page and the latter to go to some regular page. Perhaps the former is hard-coded/static and the latter looks up some URL parameter in a database.

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.



This work, the panic is only triggered when the precedence rules can't fix a conflict.

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.



But there is a precedence to pick for the panic case too.


I'm pretty sure that example worked even before the recent changes. From the documentation:

> 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.



The two routes `/foo/bar` and `/foo/*` are not ambiguous and would be allowed by the router


Are they? It seems that the latter matches the former. i.e. for the path `/foo/bar` there are 2 matching routes. One must take precedence, but this router doesn't appear to allow that.

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.



It does allow that, the one without wildcards is more specific so it'll pick that one


This pattern already works and would work with this change, you'd just need to define `/foo/bar` first.


I understood what you were saying but it might be easier to read if you escaped the * characters.


Ha! Whoops! Thanks


The panics are really annoying. Sometimes, you generate routes dynamically from some data, and it would be nice for this to be an error, so you can handle it yourself and decide to skip a route, or let the user know.

With the panic, I have to write some spaghetti code with a recover in a goroutine.



What kinds of routes would you generate dynamically that couldn't be implemented as wildcards in the match pattern? Genuine question


> or let the user know

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.



Mmmm, code with recover is just a valid code. Calling it spaghetti seems unjustified.


I'll always take a footgun with a sizable bang over a sinister undefined behavior.

Debugging the latter one is much more harder.



It's a programmer's error, so it should lead to a panic. There is simply no reason to ever handle this at runtime (as opposed to network errors or other things that can go wrong during normal operation and should be handled at runtime).


Not always, I can imagine a (weird, for sure) scenario where routes are a part of configuration, or are added dynamically at runtime.

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 I see no Harm on making this extreme wierd usecase less user-friendly and force you to go through panic recover rather than force everyone else that will never get errors to handle errors.


It's not "extreme weird", just significantly less common, so just weird, but nowhere extreme. Dynamic routing isn't exactly unheard of, and isn't some sort of perversion - servers like Traefik and Caddy do this (except that they don't use stdlib mux, of course). Basically any DIY proxy with dynamic service discovery may need it, and while people typically pick off-the-shelf solution, some write their own lightweight one.

And panic/recover is not idiomatic Go here, as it's not an exceptional situation, just an error.

YMMV, of course.



> I've (ab)used the behavior of "first to be matched wins" a ton in my career

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.



If it panics, you discover it instantly and can fix. If it didn’t panic, you could unknowingly deploy and rely on behavior you did not know about until it is an issue.


This comment is interesting to me, because in Clojure there is a data-driven routing library called Reitit, and by default it will refuse to compile conflicting routes unless you explicitly tag the conflicting routes as conflicting. So Golang's new behavior is more intuitive to me than the current behavior.

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.


I feel conflicted on it. I've abused routes in that way, but I've also been confused or encountered bugs because I didn't notice the conflict.


I would have expected the last one to override the first when registering routes. That seems to be the behavior I see most (not specific to web servers).

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?



When you launch the server. Idea is that you want to catch errors as early as possible in the development process, and by crashing the server, the programmer that wrote it will catch it.


The gorrila/mux project is weird and I am confused by it. Last year the maintainers archived the gorrila/mux project. Therefore i switched to another multiplexor called gin-gonic. Now as i saw the OP mentioning it in their blog, i went to take a look at the gorrila/mux project again to verify i correctly remembered the project to be archived. Apparently its not archived anymore, which brings the stability of the whole project to the question, or maybe it proves that Open Source is actually very stable. That if a project is missing developers, some other people can step up and start maintaining the solution.

But its nice that this functionality will be provided by Golang itself.



The ownership transfer was publicly announced in quite a few places. Most notably gorilla's blog[0], but also on Reddit[1] as well as HN[2], though it didn't get much reaction on HN.

[0] https://gorilla.github.io/blog/2023-07-17-project-status-upd...

[1] https://www.reddit.com/r/golang/comments/1528e25/gorilla_web...

[2] https://news.ycombinator.com/item?id=36935541



In my experience most developers are not connected to the development processes of their dependencies. Maybe they should be, I think there's an argument for it, but with so many dependencies there's only so much time and attention. I think any migration such as this should be done on the assumption that most people won't know it's happening.


It's a bit of a catch-22:

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.



How much time do you spend working on fsnotify if i may ask.


I spent quite a bit of time on it last year, a few weeks of full-time work or so, but haven't spent anything on it for quite a while. There's a bunch of things I'd like to do so I'll probably spend another burst of effort in the future.

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.



Have you tried executing the tests in github actions where the tests would run on different Operating Systems?


There's an extensive CI setup, but it takes forever to run. And even if it was fast, it would still be a frustratingly slow feedback loop while developing.


Just a single data point: I am subscribing to new releases for a good number of projects but it's usually binaries and not libraries. Because I use them actively and I am interested in the changes and whether they are useful or harmful for me.

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.



Yes, but the ownership transfer happened quite some time after the project was tagged as archived, which from my side was a flag to move to a solution which is actively maintained. But i am glad to see that the project is getting attention again, i liked using gorilla/mux. But its even nicer that there is going to be out of the support for similar baked into golang.


As I commented[1] I think the syntax is flawed in the proposal.

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...



But isn't it a magic string already, because of the {parameter} parsing?

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.



> And given that `Method` is either one of a few predefined constants

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



Isn’t that just a consequence of Go’s backwards compatibility guarantee?

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.



Isn’t function overloading based on the number of arguments a thing in go?

That wouldn’t break compatibility



Go does not have function overloading


Surely the changes to panic on conflicting routes being discussed upstream already prove they're willing to flex on that guarantee?


That’s not a breaking change.


How is "this api usage was allowed before and now panics" not a breaking change?


Which specific API usage was allowed before and now panics? As far as I can see the only historically allowable API usage that would now panic, is registering exactly the same path twice, which in my books is just a bug.

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.



> As far as I can see the only historically allowable API usage that would now panic, is registering exactly the same path twice.

This has always resulted in a panic[0].

[0]: https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/ne...



While I do agree it’s kind of strange, the reasoning is pretty clear. The desire to not break or change the existing public interface.


Just add a new method with a new name. Vastly better than the proposal at hand.


Is it vastly better, though?

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.



I shouldn't have to wrap a poor design choice.


That was my gut feeling as well, but honestly when using Gin, having methods like .Get(), .Post() etc. has never solved anything for me. Having a single registration method with the method in the string is probably 100% fine.


"probably" is key Here. this introduces the possibility of typo erros that can even be discovered late. Now you will need a Vet check to make sure that GET, POST etc are written correctly instead of letting the compiler to do it's job. .Get(), Post() get compiler guarantees that they are sending the right Method string.


I agree on principle, but in practice routers rely on arbitrary strings anyway. I don't see why the verb is not just a part of the route.

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.


What's wrong with automatically registering a HEAD route?


"The HTTP HEAD method requests the headers that would be returned if the HEAD request's URL was instead requested with the HTTP GET method. ". So it goes against the HTTP spec.


In practice much bigger practical problem is that no-one handles HEAD.

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.



How does it go against the HTTP Spec. The HEAD method could be autoregistered for all handlers of GET Requests and library makes sure no body is sent (Basically it's a autoregistered middleware that wraps the get handler and overwrites the reponse writer with an empty body.)

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.



Go implicitly buffers the first 512 bytes to sniff a content-type header so it's no like the status quo is free of arguably incorrect magic.


I don't like this. Is there a reason for using a stringified method prefix?

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.



Adding this is so trivial. If it really, really bothers you that much:

    func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
Obviously skipped the types for brevity.


Not a fan either. I want to be sure that routing is going to work at compile time, not at runtime.


If this turns out to be real issue (which I seriously doubt), then they'll add a vet check.


how about build time? just add a test and the "runtime" become build time.


> I don't like this. Is there a reason for using a stringified method prefix?

Backward compatibility, they didn't wanted to change function signature nor add different method for it.



If you want type safety, pick a type-safe language.


Go is type safe.


The zero-values idea, nil, reflection and stringly-typing all over stdlib and the most popular libs makes go not type safe. Were you thinking of statically typed?


Type safety is a spectrum, not a binary choice. Having used Go for 10 years now, next to other popular languages like JS and Python, I think it squarely falls into the "more type-safe than not" half of the spectrum. But it's definitely a positive development that, as this discussion shows, the Overton window of type safety is shifting towards the safer part of the spectrum.


That's true. From that perspective it is safe. But from the perspective of for example Elm and Rust, I would say go ends up in the other half of the spectrum - but still close to the middle.


I genuinely have no idea what you're referring to by "stringly-typing all over stdlib". I've written Go every day for the better part of a decade and used the standard library the whole time. What standard library functions require passing in the string of a type?


Struct tags is the most notorious example.

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.



You are right, stdlib doesn't have much of stringly-typing.

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.



We're literally in the comment thread of a new stringly typed thing being introduced to the stdlib!


I also don't prefer using strings, but to be fair, HTTP methods are just strings when the request is received. There is some beauty in that in matches the prefix of the first line of an HTTP packet


On the wire, everything is a string. Doesn't make it a good type to use!


Strings with a well defined meaning that is not being taken into consideration here beyond routing.


This is discussed broadly in the proposal.


Interesting. Now that the mux can match methods, I wonder what happens when you match a route but not a method. Do you get a 404, or a 405?

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.



I would far rather the overlapping path’s just match in the order they were defined rather than panic.

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.



Except the semantics of the language matter, and it is, actually, 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.



Why even allow registrations at a distance? I'm a complete go beginner but that feels more in the spirit of go to me: making you do something a little annoying that ends up being clear and simple.

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.



How would you propose they block it? Can you provide an example from another language where it is blocked?


> How would you propose they block it?

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.



This already exists. The point is you can still call methods on your mux instance from anywhere in the codebase


Good one less external dependency. As a side note sqlc + postgres + templ (kindah jsx for go) + htmx + tailwinds has being extremely productive stack to develop in


I’ve been having a great experience so far using sqlc.

I’ve gotta take a look at htmx.



How do you handle tailwind here? I really like the idea of adding tailwind but if it means using npm I think I'd rather just write my own css.


I'm not OP, but Tailwind ships a binary you can use (by bundling up a JS runtime): https://tailwindcss.com/blog/standalone-cli


I thought you had mistyped template there but actually no, templ is some other package different from the stdlib one. I had a quick look on its documentation and it seems quite neat!

I was hoping that bud with svelte compilation would fill this gap, but looks like templ is a nicer alternative.



I really wish that templ had intellij support! Love the idea


I think that is in the works ? VS Code support is top notch


Go is not my main tool but I've used Gin which goes like this:

    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 ?


Most likely not because there's no reason to do it this way other than backwards compatibility ...


I haven't read the article, but I would guess it's because the http header literally starts like that. You could match that against the first line, instead of having to break it into components.

It does sound like a micro-optimization.



Haven't seen in the wild. It's just bad way to do it.


Man I really really dislike this proposal. Putting the http request method into the URI... an just sometimes... oh and maybe do "POST,PUT,PATCH /something". Just no thank you. Make a dedicated method that accepts http request method names if you must do something.


I agree, does not make much sense to design it like that. The function could have another argument for listing methods or options.


Why? It really doesn't change a lot. You can't "really dislike" the whole proposal for just a stylistic issue.

> Make a dedicated method that accepts http request method names

What problem does that solve compared to embedding the method in the string?



Yes, you can. Besides the API is more than just a stylistic issue.

The problem it solves is not having string typed stuff which is a consistent pain point where it happens.



One thing I don't like about default ServeMux is that addresses are prefixes.

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?



From the proposal:

"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."



Ideally, this should be the default behavior, i.e. "/foo/" matching only "/foo/" and nothing else. And if one wants a prefix they should be able to explicitly spell it out like "/foo/{**}".

But that’s not backwards-compatible, sadly, so this is going to remain a historical wart.



Ahhh how did I miss that!!

That is amazing. Goodbye gorilla mux.



I would say that unless specified differently this bit of the documentation[1] still applies (second paragraph):

> Longer patterns take precedence over shorter ones,

https://pkg.go.dev/net/http#ServeMux



No, it does not. The length of the pattern isn't important anymore, only whether it overlaps with other patterns.


That would be a breaking API change in my opinion. I bet you the length criterion will still apply for path patterns that don't include placeholders, even if not specifically mentioned in the proposal.

[edit] Heh, I guess you'd know better. :D The comparison between the hn username and github username resolved favourably in the end. :P



Will this finally end the eternal golang http router bikeshedding?


It doesn't have the performance or the features of something like Chi/Gin/Echo, so while it will improve the lives of people who prefer to stick to the stdlib, it probably won't convince many others.


I think so. Sure people will try to value add on top, but I know at least I can just pick the stdlib and not need to go check various package repos to see if they have maintainers and open security issues. I bet a lot of developers will feel the same way.


lol no, will start the proliferation of more different abstractions over it


Not likely. Routers are so easy to write that the "different abstractions" have already been written, and are unaffected by this change to the standard library, because they don't build on the standard library, they plug in next to it.

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.



I agree on paper, but story shown how the combination of "I like it, but I want it just a little bit different" combined with something somehow new is a recipe for new weekend projects


No because we don't have named routes and the ability to reverse build the URL like in the Django router. So, at lest this functionality will be again built on top with different opinions on the best way to do it.


It seems like a very bizarre design choice, especially by an official language team, to use prefix strings instead of an enum.

For example:

  (Http.GET, "/path")
Rather than the current:

  ("GET /path")
Odd.


It's mistake they can't fix without breaking existing code or introducing new function.

But I'd prefer just a bunch of mux.GET/mux.POST/etc functions instead of that.



You’ll need new HandleMethod(method, uri, …) anyway, because otherwise you won’t be able to support any less common or non-standard methods.


Oh damn, no overloading in Go, yeah that rules out the above I suppose.

Agreed on the dedicated GET/POST etc functions though.



It adheres to the Go 1 compatibility promise.


This is a case where I, as a Go developer, would prefer to have function overloading for the sake of backwards compatibility . Because the compatibility promise is great, but the quirks caused by it aren't.


Great syntax. I didn't use any third-party routers, because implementing my own router is not a big deal, but this is just great.


It is a big deal if you want to have RESTful routing.


I wonder if the new version takes the opportunity to make the muxer faster. I needed a path matcher / muxer and I settled on forking julienschmidt/httprouter [1] into my own project, pathmatcher [2]. What I like about the original it is that it uses a trie for mapping paths to handlers, which can look up routes very quickly [3], and is optimized for low/zero allocations. My changes generalizes it (the 'handler' value can be any generic type) and exposes the underlying data structure so you can do non-http.Handler things with it if you want.

[1]: https://github.com/julienschmidt/httprouter

[2]: https://github.com/infogulch/pathmatcher

[3]: https://github.com/julienschmidt/go-http-routing-benchmark



> Therefore, the new ServeMux documentation meticulously describes the precedence rules for patterns

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.



In the example there are no concrete path, both have variables. Is there an example in the documentation that allows paths with variables precedence over concrete paths?


it is a quite form the article itself.

they say that get /foo/{id} takes precedences over path get /foo/23 or get /foo/bar



> conrete paths MUST TAKE PRECEDENCE over patterns.

And indeed they do.



I would actually have preferred that the old mux was left as is, and that the new mux had been a new package. This would have eliminated the need to be backwards compatible and allowed more freedom in creating a better API. I think that putting the method in the matching expression is a step back as it can't make use of the compiler to enforce correctness.


It's a nice change for little experimental programs, but production servers need lots of functionality that third party routers offer, like request middleware, better error handling, etc. It's tedious to build these on top of the native router, so convenience will steer people to excellent packages like Gin, Echo, Fiber, Gorilla, Chi, etc.


Honestly, there is a lot of praise of the middleware in these projects, but I recently found out that most of them are unable to handle parsing the Accept and Accept-Encoding header properly, that is: according to the RFC, with weights.

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.



Please post issue numbers, thanks!


Excuse me, what issue numbers?


Big fan of chi. It is simple, and just works. Also matches the http.Handler interface, so for testing and otherwise just makes life so easy (like using httptest new server - pass it the chi.Mux)


Stringly typed, runtime panics...


The panics occur on service startup which is good - you know immediately what is wrong and what went wrong.

The string typing thing.. I can understand why they did it even if it is not the thing I would have chosen.



Neat but I still have no reason to use it as that "cottage industry" adds more than just fancier router.


Slightly off-topic: where does one get an overview of what is planned to go into a new release?


Changes are sometimes added to the release notes doc (in the master branch) during the dev cycle, but your best bet is watching the commit stream. The release notes are much more shaped up by the time RC rolls around (see https://github.com/golang/go/wiki/Go-Release-Cycle)


[flagged]



Oracle over Google is debatable, but piss up a rope sideways is pure gold.


Most of us are running openjdk, not Oracle Java, openjdk, which is ... open...source.


[flagged]



Please don't use ChatGPT to generate your comments.


How did this kind of syntax become so popular? I think express is the first to do the scheme of:

    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.


> 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.



There's nothing "black magic" about them; they're very well documented features in Java, C#, and Python. They're officially experimental in Typescript but they're so heavily relied upon I can't imagine them getting deprecated


>There's nothing "black magic" about them; they're very well documented features in Java, C#, and Python.

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.


Your complaint is that you have to understand your web framework before writing a program in it? Is that not the case with ALL web frameworks?

> 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?



No, having to spend months learning how web frameworks are going to deal with that piece of code is not a good thing. It means that the code is readable only to those who have spent a lot of time working with that framework. And it is one thing to be able to guess what is going on, if you are only superficially familiar with the framework. It is another thing entirely to know enough to be able to debug the code or to make changes without stuff breaking.

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.



> No, having to spend months learning how web frameworks are going to deal with that piece of code is not a good thing

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



> Are you in the habit of hiring people without experience?

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.



> For one it means you can never hire people who have newly graduated.

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.



>Your complaint is that you have to understand your web framework before writing a program in it? Is that not the case with ALL web frameworks?

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.



> Most web frameworks avoid this

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



> // Middleware automatically routes "/Foo" to FooController

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?



ASP.NET Core can easily route "/hello" to HelloController.Index(), but it's not exactly automatic. The controller library adds routes to the routing middleware in a call to MapControllerRoute the app developer must make during startup which specifies a pattern.

    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.



Yes


In order for code to be maintainable it first has to be readable.

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.



> The second example has poor readability

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



> 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?

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.



> 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.

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



You appear to think that there is One Way to write applications and that anyone who disagrees with you doesn't know what they are talking about or has never had any experience with them.

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 appear to think that there is One Way to write applications and that anyone who disagrees with you doesn't know what they are talking about or has never had any experience with them.

You are wrong in your perception



I personally find that the second example has poor readability even if you are familiar with the framework/pattern. I've worked on codebases that use each of those patterns, and I greatly prefer those where all of the routes (and ideally the auth middleware too) are defined in a single top-level routes file.

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.



I can understand that view, but I like to have the controller class files themselves represent the site hierarchy. Rather than looking in a file for the appropriate route, I look through the filesystem to understand the routing. I expect FooController to map to /foo (or /api/foo or whatever). I worked on a django project which did all the routing in a single location and it frequently resulted in merge conflicts between multiple developers committing changes to the file about the same time


> is how you get hundreds of poorly written spaghetti code

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 joking, right? How is the second example better in any way?

You're also doing more in your second example. You can still do service injection in JavaScript.



The 2nd example:

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 in

2. 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.)



> 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 in

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.



You're making these statements as if the example you're presenting is the only one of its kind that has these properties. It's simply not the case. You can use middleware in other frameworks just as well. You can do input validation in other frameworks just as well. You can also class-based route handler encapsulation in other languages just as well. And I'd argue it looks _significantly_ cleaner than whatever is going on in ASP.NET land.

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.



This is straight up wrong. Documentation can be generated from either pattern. It can also do authorization if you wish so (middlewares, attributes, you can do it however you want). We get it, you like the controller pattern, but please do not post incorrect information to prove the point.


Try actually reading this time. I never said that middleware didn't exist in other frameworks, I said you can control that middleware using attributes. Do not accuse other people of being disingenuous because you didn't read something right


> You can still do service injection in JavaScript

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.



tsyringe is a popular library that does this in TypeScript


Yeah I use nestjs for my backend at my company and it also has a good DI framework, but it depends on typescript interfaces to work well


Controller style declaration is often an overkill, it's also not exactly friendly to AOT. I personally prefer using MinApi style of declaration (first example). It can do request validation, model binding and inject services too:

    app.MapGet("/", async (
        [FromBody] User user,
        [FromServices] MyService service) =>
    {
        await service.Handle(user);
        return Results.Ok();
    });


I might consider this for a very small service, but not for anything with a lot of controllers or services. When you get to the point where your backend is managing several workflows with dozens of views, dozens of models, and each with its own service, it becomes nice to be able to put these things in classes.

I also dislike functions and controllers without explicitly defined return types.



You can write a method that uses IEndpointRouteBuilder for creating a route group so that you can split up your routes by object domain or into separate classes or however you want to.

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.



I don't know about how it became popular, but personally I find the prior to be easier to parse quickly without having to learn what a handful of decorators do as well as cutting down on the amount of boilerplate required to write an HTTP handler.

Even .NET is moving towards your first example by providing minimal APIs that allow simple binding of a route to a handler function.



I like #1, I don't like #2 whatsoever


Sinatra (https://sinatrarb.com/) uses `get "/route/goes/here" do...` syntax and significantly predates express. I don't know of anything earlier, but I'm sure there are others.


Hilariously, C# used to have a framework named NancyFX which was inspired by Sinatra. The entire reason it existed was to get away from the ASP.NET MVC mess.

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...







Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact



Search:
联系我们 contact @ memedata.com