The Good, Bad and the Maybe

Webtech is always changing. The times of software sitting on relatievly the same stack for ten years are gone. You’ll be lucky if the frontend you write today lasts without major user interface churn for two years, and incredibly fortunate if the ui framework doesn’t lose developer popularity or crawl to a slog in five years. The backend ecosystem is more stable, but you might say it is just as off the Rails as frontend development.

Here’s my recommendations on JavaScript Webtech, languages, tools, libraries, frameworks, compiler-y-thingies.

Languages

TypeScript

If you can start a new project today with TypeScript, using TypeScript is easier. Migrating a legacy codebase might be a challenge, the initial warmup time learning TypeScript might feel steep, but that’s OK. You don’t need to use recursive types, you don’t need crazy interfaces. Just have it setup. Use types where they feel like inline documentation. Object shapes, arguments, and maybe that’s enough to get started.

If the tooling can support TypeScript, turn it on.

With TypeScript, you should turn on strict. It will cover so many edge cases for truthy, null and undefined checks. You will save time with this on, becuase you will avoid rewriting dozens of files.

Modern Bundlers / compiler-y-thingies

Babel and Webpack, they’re our good friends. But they’re slow now.

swc and esbuild are the fast bundlers out there today. It’s not unlikely another non-js bundler and language modifier will come along, or one of these will fall out of favor. Small webpack builds might be quick enough, but as your apps grow, your developer experience will take a hit. These two can save you from the slow.

swc and esbuild are lower level than you’re likely working on - configuring them directly is fine, and actually easier than Webpack, but you might not have to.

Modern React “Frameworks” for “Apps”

The React ecosystem is the largest frontend “framework” developer ecosystem for “apps”.

What is a “framework” and and what is an “app” anyway?

Single Page Application (SPA)

There’s a lot of debate about single page apps right now. There are all sorts of reasons why an SPA might have poor performance, directly impacting user expereince. Slow connections, high latencies, huge bundles, slow first paints, the list goes on.

For the most part, for commodity back-of-house apps, like adminitistative portals, internal dashboards, and apps are fine.

And apps!? Yes, if you’re making the next Gmail class app, fine, go ahead, make it an SPA. It will probably be fine.

When not to make it an SPA:

  • When you need your site and pages on Search Engine Result Pages (SERPs)
    • You’ll never get an SPA properly decorated in on the SERPs and properly ranked; not enough content, not enough meta tag control, no matter what they tell you about Googlebot crawling SPAs
    • If you’re thinking about e-commerce at all, you can’t use an SPA.
  • When the majority of your pages are full of static content
    • The two other scenarios cover this; server side rendering / hybrid apps, and static site generation
  • You need robust redirection
  • When you can’t make portions of your frontend code effectively public
    • This almost never happens; put special secret sauce business logic behind APIs where they belong and your treasure will stay software

CRA

create-react-app is dead.

Vite

Vite is your best bet for an SPA. It’s fast because it uses esbuild or optionally swc as of v4. Its community is thriving, its configuration out of the box is great, it supports TypeScript, it has a third of the dependencies of CRA.

I haven’t ported an application from CRA to Vite yet, but aside from TypeScript and some other high level configuration, I can’t think of anything in the actual application code that could break. In isolated new-project tests, Vite has been fast and reliable.

Webpack

You might say: “hey, I have such special needs that I must have a customized webpack option.”

You’re wrong. You do not.

Server Side Rendered / Hybrid Apps

Get your pages listed on SERPs! Get indexed today!

Rolling your own SSR with React today is not easy. Sure, you can do it, but it will be so customized, unrepeatable and unknowable. This is a Dunning Kruger decision. If you’re asking if you should build your own framework, the answer is no. If you’re asking if you should build your own framework because you have expereince with their deficiencies and have explored many available options, then perhaps.

Next

Next and Remix are the options here today.

Next has been around for a while. It has some quirks, most of them obligations of its hybrid nature.

  • routing is based on the filesystem route
  • components are not colocated with their pages
  • there are weird magical functions that run if they’re defined (getStaticProps, getServerSideProps, etc)
  • i18n is a rabbit hole
  • authentication and authorization is a deeper rabbit hole
  • middleware gets a funny runtime
  • middleware gets weird behind a cdn

The internals of Next are solid. It runs swc under its hood, so its fast enough. If you need some prerendered pages, some server side rendered pages (on demand) and some SPA-like functionality, then Next is great.

My hestiation is not the technology necesarily, it’s on the use case. The split between hybrid and spa are still pertinent. Next gives you great powers, but with great powers comes great responsbility, and in this case, new downsides to balance in your compromise. Do you really need your pages indexed? Do you have a centralized data source (instead of distributed)? Do you really have developers that are ready to solve entirely new types of problems that come up with state shared between the server, the edge and the browser?

Remix

Other than Next, there’s Remix.

Remix is still on the watch list. Hydrogen is sort of Remix too, and Remix joined Shopify. I think the tooling demos well in videos, but have not spent the time to try it and tinker with it. Let’s wait another six months and see where things land.

Static Site Generated

Brochure-ware at its finest.

If you have markdown, or some realtively narrow set of pages with data sourced from an API, static site generation works. This can break down when you need interactive editing (for that go back to Next), or when you have so many pages rebuilding them is wasteful.

  • 🔴 Not recommended: Gatsby: it feels a lot like JavaScript flavored WordPress
    • the GraphQL model is too much work
    • the mix of esm and cjs files is weird
    • the deployment tech is optimized only for their first party deployment service
  • 🟢 Tentatively Recommended: Astro: it works with everything, even React!
    • seriously, incredible work was done to ensure interop between React, Vue and others
    • uses vite under its hood, which in turn uses esbuild under its hood; new tools all the way down
    • successor development team to snowpack; well, hopefully it does better than that
    • island architecture is awesome; only load if it’s in the viewport and only this small chunk is impacted
    • you can even use plain js, with the bundler, and modern tooling, and packages… no framework at all
  • ✅ Recommended: Next: it works with everything, including static sites!
    • my card site is built with next and uses no SSR features
    • next is familiar enough to me that I jump into without reviewing a bunch of code or docs each time
    • Next shines in this regard when coupled with its parent Vercel service or a similar edge provider

React flavored State Management

Redux is a lot. Sagas are too much. Redux Toolkit makes it better, but it is still a lot.

Maybe you don’t need a service bus on the frontend.

Consider:

Tree-Local-State

Components might have state, subtrees might have state, and your entire app might have state.

For subtree, or what I’ve liked calling, tree-local-state, you can do a lot with context.

  • Context often has performance issues; you put a context at the top of your entire app, you shove stuff in like Redux and your app gets slower and slower because your entire tree rerenders
  • Context is fine if you use it as a read/write 80/20 store; auth states might be fine in a Context as long as you read and you don’t write ever again
  • Really consider Context as an escape hatch for props drilling

Server & Client Shared State

react-query is usually more than enough for fetching server side state. It provides for a great developer experience, and lends itself out of the box to a pretty decent user expereince too.

Complex State

zustand is a great option for more complex state, SPA-style cross page data propagation use cases

If you have extraordinarily complex state, Redux with Redux Toolkit has its place.

Offline

A few years ago, I was involved with multiple projects that insisted client side state persistence for offline support. Despite all these years of development and industrial growth, I still think offline webapps are a very poor experience. Users have no idea what’s going on. Engineers barely have any better tools to make it a decent experience. In general, if offline is where your web app will be used, give all that PWA business a try and enjoy the fruitless pursuit of the perfect sw.js, but also seriously consider a real app or being dependent on generally available Internet access.

Server Side / API

With something like Rails, you can have your frontend and your backend all sort of as one thing. Even though the JavaScript ecosystem is uniquely suited towards driving the frontend and backend, the interop bridge between is still clearly in active development. Just look at Next and Remix for those examples. Even if you consider those a “backend”, they’re not the whole story. There’s no Rails. There’s no Laravel.

If anything, Next and Remix are the fabled Backends for Frontends dream fufilled, but instead of being an composition API, its composing data from API into UI. What about the actual backend, where your database lives and you interop with your own API friends?

Nest

Historically, I have promoted Nest as a viable option for an actual backend.

For Nest, I have unresolved issues, even after years of trying:

  • Annotations are half baked, not spec, and feel ergonomically odd in the language
    • They’re great when they’re magic you consume, but if you need to go any deeper, they’re just so obtuse, and it certainly does not help that TypeScript and JavaScript have no runtime reflection, so its just an illusion
  • Silly defaults
    • Why was strict off?
  • The Nest service container is kind of weird, it uses, well ironically now that I think about it, nested modules that isolate services into logical units
    • Dependency Injection (DI) isn’t for everyone, though it has its uses
    • With Spring, it all lived in one service contain freely, no modules required
    • The register/bootstrap process, unlike Laravel, isn’t clear and it’s not well described
  • TypeORM, the ORM that was highly promoted in the first party docs, was effectively abandoned
  • Integration with npm ecosystem tools is often painful
    • There are adapters for many popular libraries and patterns, but not everything
  • Dependencies… so many… dependencies
    • The more dependency surface area you have, the more time you spend auditing your security than building something useful
  • Nest still ships without a modern compiler backend (esbuild, swc) making it slower than it needs to be
  • Nest ships without new options for ESM

Arguably it is valuable in a larger team setting because it provides much needed structure. Files go here, they’re named this way, there are interfaces you can recognize and research.

I think you can use it, but you need to make the choice. The hard choice.

Express

Yep, good old Express. Express 5 will never come out of beta. It uses an API design that’s from the early days of Node and we’re stuck with it for now.

Popular middleware like Passport are beginning to age out, which is great, because the momentum that express brought will age out too and something else can come along.

Other backends

Well, why not..

  • Fastify
    • It’s definitely work looking into, but everyone knows express
  • Koa
    • Effectively a rewritten fork of Express, with async out of the box
    • Too much taken out of the box unfortunately

In the end, if you use express, with typescript, with swc, I think you have a winning combination to explore your ideas and grow your team. Then you can port that into or Go or Rust or whatever. That will be fine.

Package Managers

  • npm has a legacy of poor performance, lagging feature sets and not being yarn
  • yarn has a legacy of being great, being fine, and having no viable upgrade path to any later version
  • pnpm is probably fine
  • berry is probably fine, but you will have to learn more about it

In many ways, npm vs yarn is like maven vs gradle, everyone picks gradle but maven hosts the registry and works as a tool OK too.

For now I recommend yarn, and while I have tested berry I have not properly field tested it against real codebases.

Validation

Validation is often overlooked and undercooked.

  • 🟠 ajv
    • pro: json schema oriented
    • pro: TypeScript support
    • con: limited support for imperative validation
    • con: moving target
    • con: verbose
  • 🟠 yup
    • pro: easy to get started
    • con: mixed support for complex validations (or imperative validation)
    • con: major type-rewrite in recent history
  • ✅ zod
    • pro: easy to get started (similar to yup)
    • pro: there are adapters to convert zod schemas into json schemas

Schema validation is not the same as business logic validation. For example, if you have a schema {count: number}, that’s pretty easy to validate for all of these libraries. Suppose you need to validate the number is in a range. Also easy. What if the number has to in a fixed range, and also related to a value in a database? That feels like it is outside the scope of schema validation, but having schema validation that could do this, or share its error structure with you is worth it.

For now, I recommend zod.

fetch and friends

For hybrid apps (with Next), I recommend fetch. Out of the box, Next will provide fetch in its middleware and server side. fetch will work in the browser on the frontend. fetch is great and all, though the developer experience is not always so productive. Consider building your own middleware and your own intermediate API layer to handle common situations and oddities.

For an SPA, I recommend axios. Axios has been a go to library for years. Likewise, for many years it felt like it was abandoned, languishing in a 0.x version. Now that it is 1.x, I feel better about it. I hope a 2.x variant comes around with fetch internals.

got and ky might be awesome. However, part of the ecosystem is locked out of using their latest versions because of ESM incompatability.

Follow me on Mastodon @ryanmr@mastodon.cloud.

Follow me on Twitter @ryanmr.