
Leaving Python for JavaScript - jgalvez
http://hire.jonasgalvez.com.br/2017/Aug/25/Leaving-Python-for-JavaScript?
======
ageitgey
So a person who writes web apps prefers JS over Python. Cool. Pick the right
tool for the job. If your job is writing client-side-heavy web apps that share
code between the client and server, then JS might be a better choice than
Python.

Look, writing programs that respond to HTTP requests with HTML is not rocket
science. You can do it in JS, Python or Ruby and be very productive. If you
hate yourself, you can do it in Java, Go or whatever else and trade server
performance for development speed. If you really hate yourself (or are Amazon
circa 2004), you can write your whole website as a C++ static binary.

My theory is that the endless proliferation of JS libraries, frameworks and
language revisions are because lots of smart and talented developers are
employed to build websites - but building websites is just not that
technically interesting, so they express their creativity by creating new
tools, frameworks, package managers and ES revisions. It's a symptom of
boredom.

But my one bit of career advice would be don't "Leave X for Y." Don't tie your
livelihood and career to a single X or Y. Learn how to use a variety of tools
and pick the best one for each job. Any big project will require using a few
different tools and really valuable developers are the ones who can navigate
that entire landscape intelligently.

~~~
rlander
Right? When I read these kind of posts I always imagine a carpenter saying the
equivalent "After over 10 years using a hammer as my main tool, I have moved
on to the screwdriver." Which is nonsense.

~~~
z3t4
With programming languages you can both screw and hammer. I'ts not like you
need to know C++ to do GUI's and D to make CLI's, or the other way around.
What's the best tool for the job when all tools does basically the same thing
? I would say the tool you are best at.

------
monkmartinez
I started programming in JS then moved to Python. It seems like JS has become
very, very complicated. I look at his post and I am lost... I remember
Knockout.js and Node in the v0.8 days.

I mean look at this:

    
    
      dotenv (lets you load the environment from an .env file), axios 
      (HTTP client library), cheerio (HTML parsing library), bcrypt 
      (password hashing), co-body (HTTP body parser), co-busboy (HTTP 
      multipart parser), jsonwebtoken (JWT generator), koa-jwt (JWT 
      middleware), koa-router, koa-sslify, vue-no-ssr and source-map.
    

What the hell is all that? I am not saying he is wrong... I am just saying
that I don't have the capacity to play in that playground. I am also asking if
any of those libraries will be around in a year?

~~~
alayek
I have been in your position about a year ago. The approached that helped me,
was to not worry about all this.

Just start somewhere; and in a month or two, you would mostly know which
libraries you need to add to your project.

The best way to go about it, is to check a few popular open source projects;
what libraries they typically use.

> I am also asking if any of those libraries will be around in a year?

You can check the download stats of these in
[https://npmjs.com](https://npmjs.com), and activity on the GitHub repo,
before adding it to your project.

If they are still around, but have published a new major version to update a
few APIs; you can slowly migrate to it.

If not, you'd read some Medium post or Hackernews discussion, about what is
replacing that.

There's something called Greenkeeper
([https://greenkeeper.io/](https://greenkeeper.io/)) that integrates a GitHub
bot to your repo, and files a PR when something needs to be upgraded.

You can implement something similar on your own, if you aren't interested in
using something like this.

~~~
secstate
I mean no offense, but the "just use it you'll get used to it" strategy seems
the antithesis of the goal of engineering as a discipline.

A materials engineer simply cannot afford to pick concrete unless she knows
the specific load and weathering characteristics of it. The same should be
true for software projects. You shouldn't use cheerio because everyone uses it
in their project, but because you've looked through the source and considered
all your options for need it fills.

Sorry for the rant, but I think it's important to encourage software
developers to think before they code.

~~~
Zyst
It's important to not disregard the expertise of a fellow developer before you
have decisive proof that their approach is genuinely poorly thought out. That
is to say: Give people the benefit of the doubt.

As an outsider the big amount of dependencies look daunting, but the tendency
in JavaScript as of late has been to use small libraries that do one thing, do
it well, and play well together with others (Composable is the buzzword).
Whereas Java and C# for instance tend to have macro libs that "Have it all
bundled in".

It's different approaches, and you might not _need_ absolutely everything,
then again you might, and you are in a much worse position to evaluate that as
an outsider than the developer who made it.

All in all, I quite like the JavaScript approach where pieces are just that,
instead of a pre-built lego set which I'll have to study to build, I have
small pieces which give me more flexibility to take my project where I want to
take it. The tradeoff, and they naturally exist, is that dependency
maintenance is substantially more difficult, and the likelihood of something
you use being straight up abandoned raises for each dependency you add. Which
might lead to having you either maintain that piece yourself, or abandon it,
and add another piece that fulfills a similar role.

------
Walkman
> there's no acceptable way to pass a function body to another in Python.

There are sentences which are telling a huge incompetence about the person
using these.

This sounds the same story to me as "We used tech X, but X is shit/can't do
something so we switched to Y, and Y is awesome and fast." Where the person
switching was just incompetent with X but it would have been perfectly
solvable and they just do a totally different thing with Y.

In the specific "passing a function" case, yes, it's impossible to pass only a
function body, but I think it's impossible to do in JavaScript also? (correct
me if I'm wrong), because you pass in function. You can do the same in Python
by defining a function in the same scope and passing that in. If you don't
like that, it's just one opinion, not necessarily good or bad. For example, I
find it ugly and difficult to handle nesting this multiple times (defining
another anonymous function inside an anonymous function) which is called
"callback hell" if I understand right, but that's just my opinion also,
because people seem to able to accept this hell. :)

~~~
neurotrace
No one likes callback hell but I believe they meant that you can just pass a
function expression directly. No need to define a named function. It's the
difference between:

    
    
      let doubled = nums.map(x => x * 2)
    

and this:

    
    
      def double(x):
        return x * 2
      doubled = map(double, nums)
    

(my Python is pretty rusty so maybe there's a better way of doing that)

~~~
takeda
You can do the exact same thing in Python, and this is not any new feature:

    
    
        doubled = map(lambda x: x * 2, nums)
    

JS it's just not as explicit with what you're exactly doing.

~~~
gknoy
If you want more than a one-line lambda, however, you're in for a rough time,
and need to go the route of

    
    
        def foo():
            # ... four lines here ...
        modified = map(foo, items)

~~~
pseudalopex
What's rough about giving a function a name?

~~~
gknoy
Naming things is one of the hard things in computer science. :) Anonymous
functions, when appropriate, let us avoid having to think about that.

For example, `f` is a refactoring of an anonymous function:

    
    
        function f (item) {
          return {
            // foo properties
          }
        }
        const fooItems = values.map(item => f)
    

Imagine that you have to map over several different collections of items, and
generating the lists `foo`, `bar`, and `baz`. You have to either name the
functions we pass to map `f`, `g`, and `h` (which I bet our reviewers would
hate), or `fooMapFunc`, `barMapFunc`, `bazMapFunc`. It just pollutes the
namespace that I have to keep in my head, because I have to wonder "is this
used somewhere else?".

Moreover, is it _more readable_ to define the helpers first, and then the
collections that are made by using them, or to define the pairs (fooMapFunc,
fooItems) in sequence? This could easily be felt one way or the other, and
both are valid, but leads to code review holy wars. "I feel this is more
readable / I feel exactly opposite".

For comparison, the anonymous version of this similar operation:

    
    
        const fooItems = values.map((item) => {
          return {
            // foo properties
          }
        })
    
        const barItems = values.map((item) => {
          return {
            // bar properties
          }
        })
    
        const bazItems = values.map((item) => {
          return {
            // baz properties
          }
        })
    
    

In this case, the anonymous function is clearly not usable anywhere else, so
it's easier to be sure that it's something one can change, and completely
removes the chance of holy war over readability of whether to define helper
functions together or with their collection.

~~~
pseudalopex
Naming things is hard because names represent abstractions. Complex
abstractions are hard to get right and hard to summarize; widely-used
abstractions and names are hard to change.

A nested function is a simple abstraction in a narrow scope. Both the
abstraction and the name are easy to change, so the name only has to describe
what the function does now. When that's hard, it's often because the function
does too much, does too little, or belongs somewhere else. Likewise, people
arguing about the organization of inner functions is usually a sign the outer
function should be refactored. Anonymous functions are appropriate when names
would be almost redundant, not when coming up with a name is hard.

My editor can show me where a function is used; it can't summarize what a
function does. Either way, you have to find where "fooItems" is used to know
if you can modify the function.

Conversion functions are very easy to name ("itemToFoo"). Idiomatic Python
would just use a loop:

    
    
        foo_items = []
        for item in values:
            foo_items.append({
                # foo properties
            })

------
rdudekul
A lot of innovation happened in JavaScript space over the past few years and
yet the learning curve is high. I prefer to use Python for Data Analysis, Data
Transformations & Machine Learning. For web applications, Node/Express +
React/Vue seems to work better.

~~~
jgalvez
Yep, Python is still king for machine learning.

------
abuckenheimer
> So with JavaScript you've got arrow functions, the method shorthand
> definition syntax, the spread operator, destructuring assignments, all
> functional Array methods and async functions.

This confuses me quite a bit, there are nuanced differences between these
ideas in the two languages but at the surface these are things that very much
exist in both languages

 _arrow functions:_

    
    
      (x,y) => { x + y } 

vs.

    
    
      lambda x,y: x + y
    
    

method shorthand definition:

    
    
      MyObj = {
          foo(x,y) { return x + y }
          bar(x,y) { return x * y }
      }
    

vs.

    
    
      class MyObj:
          foo(self, x, y): return x + y
          bar(self, x, y): return x * y
    

_spread operator:_

    
    
      function add(x,y) {
          return x + y
      }
      add(...[1,2])

vs.

    
    
      def add(x, y):
          x + y
      add(*[1,2])
    

_destructuring:_

    
    
        [a, b, ...rest] = [10, 20, 30, 40, 50];
    

vs.

    
    
        a, b, *rest = [10, 20, 30, 40, 50]
    

_functional array methods:_

    
    
        forEach(["Wampeter", "Foma", "Granfalloon"], print);
    

vs.

    
    
        list(map(print, ["Wampeter", "Foma", "Granfalloon"]))
    

_async methods:_

    
    
        function resolveAfter2Seconds(x) {
          return new Promise(resolve => {
            setTimeout(() => {
              resolve(x);
            }, 2000);
          });
        }
    
        async function add1(x) {
          var a = resolveAfter2Seconds(20);
          var b = resolveAfter2Seconds(30);
          return x + await a + await b;
        }
    
        add1(10).then(v => {
          console.log(v);  // prints 60 after 2 seconds.
        });
    

vs.

    
    
        async def resolve_after_2_seconds(x):
            await asyncio.sleep(2)
            return x
    
        async def add1(x):
            a = resolve_after_2_seconds(20)
            b = resolve_after_2_seconds(30)
            return x + await a + await b
    
        loop = asyncio.get_event_loop()
        loop.run_until_complete(add1(10))
    

There's a lot of fun differences between the origins of these features and how
they work but the sentence to me doesn't quite to the idea of "Leaving Python
for JavaScript" justice

------
dharness
Why is all the actual content on the far right side of the page? Even more
interesting given that you specialize in UI/UX.

~~~
jgalvez
Hi, it's just a blog, not a paid job. Not a lot of effort went into it. I'll
take a look when I have a chance (it looks fine on my MBP and several others).

~~~
sushisource
I don't think it's broken. It just looks pretty cramped and weird if you've
got your browser in a portrait orientation.

------
_greim_
With async/await now landing in mainstream engines, Koa instead of Express,
the new Turbofan+Ignition optimization pipeline "evening out" V8's performance
characteristics, and ES6 enjoying reasonably broad support, I feel like JS is
now crossing a threshold into being not a completely sucky programming
experience, which is a sentiment this article echoes.

I'm probably being overly optimistic, but I look forward to a future where a
"naked JS" movement rises up, similar to "vanilla JS", but rebelling against
tooling complexity instead of jQuery bloat, in which your website's /js/
folder contains pretty much exactly what's in git.

~~~
spondyl
I'm split on it myself. I like being able to go Right Click -> View Source and
poke around but with more modern sites, you're given nothing but a single JS
file. You can scroll through the DOM in developer tools but it's not as neat
as text :(

------
dagenleg
> there's no acceptable way to pass a function body to another in Python.

What. Why would you ever want a function body as a string? What kind of vile
sorcery are you doing? One can use inspect.getsourcelines in python if really
pressed to, but I find it horrifying that javascript developers actually seem
to require this functionality often.

~~~
saurik
The author did not say "as a string" and I am extremly confused as to how you
read it like that... what? Essentially, the complaint (which is extremely
common) is that functions that contain statements are not expressions in
Python, so functional coding styles using things like .map() and .reduce() end
up fragmented (and you are horribly tempted to play extremely insane tricks
with decorators to build something that feels like a Frankenstein Ruby block);
this is why the author then brings up arrow functions: it has nothing to do
with strings.

~~~
aaronchall
> the complaint (which is extremely common) is that functions that contain
> statements are not expressions in Python, so functional coding styles using
> things like .map() and .reduce() end up fragmented

You mean these critics find

    
    
        >>> map(lambda x: x**2, filter(lambda x: not x % 2, range(11)))
    

preferable to the "fragmented":

    
    
        def square(x):
            return x ** 2
    
        def is_even(x):
            return not x % 2
    
        >>> (square(x) for x in range(11) if is_even(x))
    

Both result in the same lazy iteration (to see results below - requires
materialization with a constructor like list or tuple), but I find the
"fragmented" approach much more readable (so long as the functions do what
their name says they do - I _have_ seen Python where they actually did the
opposite...)

    
    
        >>> list((square(x) for x in range(11) if is_even(x)))
        [0, 4, 16, 36, 64, 100]
        >>> list(map(lambda x: x**2, filter(lambda x: not x % 2, range(11))))
        [0, 4, 16, 36, 64, 100]

~~~
Symen
But your lambda example would be more clear if collections functions supported
chaining. You can add a small comment if you want to be explicit. This way the
code reads linearly, you don't have to wonder why a "square" function and
"is_even" function are defined before you see how they are used.

    
    
        # square even numbers
        range(11)
          .filter(lambda x: not x % 2)
          .map(lambda x: x ** 2)
    

However such a chaining API is not practical in python, because lambda syntax
is voluntarily crippled to one expression only.

~~~
pseudalopex
Long method chains are impractical in Python because of the line continuation
rules. That's orthogonal to the question of whether complex functions should
be required to have names.

~~~
Symen
There are a few gotchas but you can do it. Mostly you need to put you multi-
line chain call inside parentheses if it is not already inside a function call
or data structure (similarly to generator expressions).

For example I often end up doing:

    
    
        something(
            "Template string {thing}"
            .format(thing=33)
        )

~~~
pseudalopex
It's _possible_ but rarely more concise or clear than just using variables.

    
    
        label = "Template string {thing}".format(thing=33)
        something(label)

~~~
Symen
I guess it is a matter of preference at some point.

Storing each step in variables has the advantage to be self-documenting and
nicer when debugging. However in many cases I feel like wasting energy trying
to find short and adequate variable names for each steps in a computation,
especially when the steps are clear enough by themselves but difficult to
describe in 1-2 short words.

You also need to keep the variable names in sync when refactoring, which may
cause even more refactoring if the line gets too long with the new name.

IMO it makes sense to use both styles where they feel most adequate.

------
jjnoakes
I want to like JavaScript, but the type coercion land mines and the lack of a
real numeric tower make me a little queasy. When I have to use JS I stick to
the transpile-to-js languages mostly for those reasons.

~~~
iffycan
Is TypeScript one of your goto "transpile-to-js" languages? I'm a Python
developer who is learning to love TypeScript. With TypeScript I feel like I
can write maintainable, collaborateable JS.

~~~
jjnoakes
TypeScript has the same number problem that JavaScript has.

------
yiransheng
ES6+ javaScript in absorbed many great features from python: destructuring vs.
tuple unpacking, spread (...) vs *args argument unpacking and generators. Many
things I enjoyed in python are now also idiomatic javaScript. Only thing
missing is function decorators:

    
    
        @connect(...)
        function StateLessComponent(props) {
    
        }

------
freecodyx
I did the move but to GOLANG instead.

I think what is more important than the language it self, is the toolings and
standard library.

every now and then you hear about a new framework, a lot of hype, which is not
for big projects.

I think the wise thing is to move to golang

~~~
monkmartinez
Why? I mean please elaborate.

------
devnonymous
> So with JavaScript you've got arrow functions, the method shorthand
> definition syntax, the spread operator, destructuring assignments, all
> functional Array methods and async functions.

In language that pythonistas would grok:

Arrow functions: multiline lambda definitions

method shorthand definition syntax: class defined with only classmethods, or
in other words a namespace

the spread operator: *args in functions

destructuring assignments: tuple unpacking... But also for creation of
instances

all functional Array methods and async functions: functools and async... Plus
a bit more

While most of this is good to know, I'd still stick with python for
readability and maintainability purposes.

~~~
creatonez
IMO arbitrary rules like lambda expressions being limited to one expression do
not directly improve readability and maintainability. Consistently using good
conventions in the first place will help you more than arbitrary restrictions.

And to be clear, I would classify, say, Rust as not having many arbitrary
restrictions. All the limitations that Rust has compared to C fit clearly
within the language's core design. On the other hand, I don't see this same
structure and consistency to some of Python's rules.

~~~
dragonwriter
> IMO arbitrary rules like lambda expressions being limited to one expression
> do not directly improve readability and maintainability.

In Python’s specific case, I've seen no alternative that isn't bad for
readability; the strong line-orientation of Python's broader syntax limits the
good options for inline anonymous functions.

That being said _complex_ lambdas in general can be adverse to maintainability
and Python’s single-expression limitation largely prevents that (though you
can make hideously complex opaque single-expression lambdas if you try.)

------
wolco
If you started with php instead of python you would never have left.

Vue is a great language but php can offer more in the backend

------
kochandy
Thanks for your article. It's really helpful to hear about the technologies
that have been used in actual projects/production environments. I'd love to
hear more about what diminished your excitement with ElementUI vs iView

~~~
jgalvez
Mostly aesthetics in the default theme. Both are actively maintained and the
APIs are super similar.

------
yupyup
When reading these "Leaving X for Y" I first think that maybe some people are
unable to fluently use more than 1 language.

Seriously, professionality is being able to use the right tool for the job at
hand.

------
metaphorm
why is the sidebar on the left half the width of the page? I can hardly read
the main content crammed into the right half.

------
deathanatos
> _the spread operator_

How?

JS:

    
    
      var args = [0, 1, 2];
      myFunction(...args);
    

Python:

    
    
      args = [0, 1, 2]
      myFunctions(*args)
    

A more complicated one:

JS:

    
    
      var args = [0, 1];
      myFunction(-1, ...args, 2, ...[3]);
    

Python:

    
    
      args = [0, 1]
      myFunction(-1, *args, 2, *[3])
    

As used in list building; JS:

    
    
      var parts = ['shoulders', 'knees']; 
      var lyrics = ['head', ...parts, 'and', 'toes']; 
    

Python:

    
    
      parts = ['shoulders', 'knees']
      lyrics = ['head', *parts, 'and', 'toes']
    

"A better way to concatenate arrays"; JS:

    
    
      var arr1 = [0, 1, 2];
      var arr2 = [3, 4, 5];
      arr1 = [...arr1, ...arr2];
    

Python:

    
    
      arr1 = [0, 1, 2]
      arr2 = [3, 4, 5]
      arr1 = arr1 + arr2
    

(Though the star notation works here too.)

I omit JS's use of ... on objects; Python's class system works differently —
and IMO, more rigorously — than JS's, making ... less sensible on Python
objects. (Python focuses much less on passing around untyped key-value bags
and more on strongly typed classes IMO; both are possible in both languages,
but the idioms around them differ, and I think Python's idioms tend more
towards having a well defined class with well defined attributes, and not
having just a bag of attributes, moreso than JS at least, and I feel that
direction (well defined classes) leads to more robust code. In particular, it
forces you into naming your concepts, and defining their set of attributes: a
simplistic type definition.)

> _all functional Array methods_

Python has map, reduce, etc., as well as generator and list comprehensions
which are often easier to use.

> _async functions_

Python and JavaScript have practically identical syntax in this area.

> _there 's no acceptable way to pass a function body to another in Python_

I find it very acceptable that if your function body is more complicated than
an expression, that you're forced to pull it out and name it, frankly. I think
it does good things for fighting complexity, and this just isn't something I
worry about day to day while using Python. But I will concede that Python does
lack a syntax for passing a function in an expression context.

(But I would also note JS's screwed up named-function syntax; in Python:

    
    
      def foo():
        # body
    

is a valid statement. JS's:

    
    
      function foo() {
        // body
      }
    

is _not_ a valid statement, and can only appear in certain, particular
contexts. In particular, the following is not valid JavaScript, though many
implementations will do The Right Thing™, I'm told:

    
    
      if(true) {
        function foo() {
          // body.
        }
      }
    

[2])

> _My code usually revolves around higher order functions, reduce(), map(),
> etc. I can 't remember the last time I wrote a regular for loop in
> JavaScript._

Because JS for a long time (until ES6's for(… of …) syntax) lacked a for loop
(the C style look, and for(… in …), do _not_ count, as they do semantically
different things), which is why you're using forEach().

> _arrow functions_

The best thing about arrow functions is the sane binding of `this`, a problem
notably absent in Python to begin with.

> _[Python 's] class definition boilerplate is still hard to look at_

This is sometimes true; I find the attrs[1] package helps greatly here for
small, struct-like classes.

[1]: [https://pypi.python.org/pypi/attrs](https://pypi.python.org/pypi/attrs)

[2]: [http://kangax.github.io/nfe/#expr-vs-
decl](http://kangax.github.io/nfe/#expr-vs-decl)

------
ojhughes
Poor misguided person

~~~
crimsonalucard
I use to read hacker news and feel that all the articles were by people
infinitely smarter than me. Nowadays I occasionally hit something like this.
The fact that this makes it to the front page means many programmers are
equally misguided...

------
scriptkiddy
Just in case the author is present in the thread:

Your blog is extremely difficult to read on wide screen monitors. The entire
left 50% is basically just blank space. My suggestion is to make the left side
maybe 20% width on desktop or less.

Also, regarding the article content:

There really aren't any good reasons presented here about why you moved from
Python to Javascript for back-end web development. It seems that you simply
prefer Javascript semantics. There's nothing wrong with that, but the title is
misleading considering that the bulk of the article is actually about which JS
tools you use on recent projects and not why you moved.

Also, could you clarify what you mean by

> Also, there's no acceptable way to pass a function body to another in
> Python.

Functions are first-class in python and can be passed as arguments to other
functions. I'm not sure I understand what you mean with the above statement.

Cool to see someone else has been using Nuxt though.

~~~
jgalvez
> Your blog is extremely difficult to read on wide screen monitors. The entire
> left 50% is basically just blank space. My suggestion is to make the left
> side maybe 20% width on desktop or less.

Thanks, I'll take a look when I have a chance. I myself don't own a widescreen
monitor, the site looks fine on my MacBook and several other computers. It's
not a paid job, not a lot of effort went into it ;)

> but the title is misleading considering that the bulk of the article is
> actually about which JS tools you use on recent projects and not why you
> moved.

Hm, actually, it's not. I'm surprised by this comment. I point out precisely
the JavaScript features that weighed the most in my decision: "So with
JavaScript you've got arrow functions, the method shorthand definition syntax,
the spread operator, destructuring assignments, all functional Array methods
and async functions. Combined with Vue's minimal patterns and Nuxt's
conventions, I can't think of a better language to write web applications in."

> Also, could you clarify what you mean by > > Also, there's no acceptable way
> to pass a function body to another in Python.

Exactly that, a "function body", "closure", "inline function", whatever you
wanna call it. In Python there are lambdas, but they're limited and not even
encouraged anymore. We're left with passing function references, and that's
where JavaScript comes out the winner IMHO.

~~~
pseudalopex
"Anonymous function" is the term you're looking for. Lambdas are still
encouraged for trivial functions; the only thing that's changed is that
comprehensions have replaced many uses of map and filter. Python just requires
you to name non-trivial functions.

IMHO, complex/nested anonymous functions are usually bad for maintainability,
so I'd rather have a language that doesn't allow them (Python) than a
community that overuses them (JavaScript).

~~~
scriptkiddy
I couldn't agree with you more. I think that Python's lack of complex
anonymous functions actually forces the programmer to compose their code in a
more efficient manner.

------
holydude
I am still learning to like Javascrip and its ecosystem. I do not think I
would be able to invest time in transpile-to-js language (looking at you
TypeScript). I really hate the evolving ecosystem, hyped libs / frameworks and
tons spaghetti new paradigms.

~~~
creatonez
>I really hate the evolving ecosystem, hyped libs / frameworks and tons
spaghetti new paradigms.

Then don't use them. The javascript ecosystem on the backend and frontend has
settled down quite a bit. Use something that's been established for a while
(React, Vue on frontend or Express, Koa on backend) and you're not gonna
suffer from churn too badly.

