Ultra Simple JS & TS Development Philosophy
This is a collection of concepts for writing readable, maintainable, frindly Node JS-based projects. This includes frontend JS projects, TypeScript projects, Node JS backend projects, anything with a package.json
.
JavaScript is a very wide domain, the possibilities of coding styles and tooling are quite vast. There is no “Ruby on Rails of JS”, and therefore the ecosystem is quite literally lacking “rails” — a guiding set of constraints that make things predictable and master-able.
The goal here is to outline some philosophy that applies across these types of projects.
The overall philosophy: use constraints to make things predictable, avoid shiny things, keep things as simple as possible.
1. Simple scripts
In your package.json
, just use simple predictable values in the scripts
section.
Use npm start
and npm build
. That’s enough.
No npm run dev
or npm run start-dev
npm generate && npm build-dev
.
If your codebase gets big, and/or has a lot of scripts, you can put arbitrary JS files into the scripts
folder.
Above all, make sure that:
npm start
starts your application.npm run build
builds your application.npm run test
runs your test suite.
NODE_ENV
2. Use There’s no need for a dev:true
flag on your library. Use process.env.NODE_ENV = 'development'
.
There’s no need for process.env.NODE_ENV
to be anything besides development
or production
.
3. Don’t use weird language features
Build tools and bundlers are helpful. But JS compilers like babel
often come hand-in-hand with bundlers. This creates a problem: babel
allows us to use whatever language features we want, even, perhaps, language features which are not yet part of the language.
This is bad.
babel
will eventually dissapear, as JS transpilation becomes less necessary.
Aim for your code to work in both Node JS runtime and browser runtime.
Avoid special language features which are not officially part of the Ecmascript 262 specification.
4. Typescript = JavaScript with types
Building on item 3, the same goes for any TypeScript code you write. This means, for example no TS decorators.
Think of TypeScript as JavaScript with types. It’s not JavaScript with types and special language features. Stick to the spec to avoid future headaches.
You don’t want a teammate somewhere in the world or in the future feeling 😞 frustrated after trying for 6 hours to get your code to compile or run in an environment somewhere outside of your particular TS build chain of what was hot in November of 2019.
5. Avoid source-to-source compilation
My worst experiences in software development have been using source-to-source compilation. You might also refer to these things as “meta frameworks”.
Frameworks are good. Meta frameworks are bad.
If you’re using a compiler or a framework that compiles to a source code format, i.e., it generates source code, be prepared for headaches. The “generated code” from tools such as website builders is usually unreadable and unchangeable by the human coder for similar reasons.
Reminder: JS can be considered a compile target, like in the case of Typescript. That’s okay.
But, for example, a tool that “generates” React components will create lots of problems for you down the road. These problems are usually:
- The code is not readable
- Code generation is one-directional (can’t change the generated code, then generate again)
- Generated code is usually unable to integrate with other code or parts of your application
- Generated code is usually brittle, easy to break
- Poor future-proofing (the tool or meta framework may not be well-prepared for future dependency updates; may deprecate support or active development)
- Lock-in, hard to switch away from or support is deprecated
- Performance issues!
6. Avoid import path aliases
Referring to this pattern:
import someModule from '@/my/components/someModule'
This will create headaches in the long run. Is it really worth it for the refactor convenience?
7. Always ask: can the platform do that for me?
Before installing a dependency, always first check if the platform you’re running this code on supports your need natively. This is especially imperative because of the prevalance of superfluous NPM packages in the JS ecosystem. Many, many things that used to require an external dependency to achieve are now supported natively in the browser or Node JS.
For example, before adding a react-loadable
, research how to achieve bundle splitting directly using an ES Module import
statement. Before adding a library for lazy loading, try implementing using the browser’s built-in features for lazy loading images.
If you look closely, and understand the primitives available to you on the platform, you can accomplish quite a bit. As an example, Map
is faster than Object
for many operations, building your application around this principle may save you massively on performance in the long run.
node_modules
8. Open This is a weird piece of advice. TODO: describe this