I think about it as, there's a bunch of code you want to run to serve a webpage: Business logic, SSL termination, caching, lets-encrypt, load balancing, etc etc. Separately from the question of
what you want your web server to do is the question of how you cut up the cake of all that stuff. It makes the most sense to write some of this code (eg caching) in highly optimized native language. And it makes the most sense to write your business logic in whatever language your team is most productive. (Eg python or ruby or whatever).
The question of process architecture (how you cut the cake) has a ton of options! For example, a nodejs process can:
- Run other javascript code by including it via npm. (Directly or in a worker thread)
- Run C code by compiling it to asmjs and including it like any other javascript code
- Run C / Rust / Go code by compiling it to wasm and including it via the Webassembly constructor
- Run C code by compiling it to a napi module, and linking to it via nodejs's native modules API
- Run code in any language in a child process (via spawn / exec) and communicate to it via stdin / stdout pipes
- Run code in a sibling process and communicate via named pipes, IPC or via network requests
There are engineering tradeoffs with all of these options. For example, wasm works in the browser, and isn't CPU architecture specific. But wasm tooling is language specific. And it runs about 3x slower than native modules (which don't work in the browser). Child processes run at native speed and work with any language, but "calling" into the child process is much more complicated than calling a wasm / napi function. Errors behave differently too - an uncaught exception in an npm module will often crash your whole nodejs process. But a segfault in a sibling process will just crash that process. (And its not obvious which one is better!). If you want to, you can include the logic for SSL termination, caching, etc in the same programming language as everything else you're doing - which means writing your business logic in C, or your caching logic in javascript. This has runtime performance vs productivity implications. Rust is great for this - but link times get pretty bad.
In short, there isn't a "right" answer to any of this. Basically all of these options are viable ways to divide up your program. When I was getting started, apache + CGI was the common approach. (Your logic ran as a child process of the web server). Now, application servers have complete HTTP stacks and its common to do interop between your app server and nginx via HTTP. It works pretty well, but there's nothing essential or obvious about that choice either.