Skip to content
Fred Zhao
TwitterLinkedIn

How I wish I got introduced to JS development in 2025

20 min read

Recently I talked to other software engineers looking to build some simple apps, and recommended RedwoodJS as a simple starting point (based on this earlier post).

As I was sending it to people who may have never written production JavaScript, I realized that there were quite a few decisions to make and understand... many of which I didn't really understand myself either! And that's despite working in JavaScript at Facebook and Airtable over my career -- the last 4+ quite deeply, since Airtable is on a fullstack JavaScript codebase.

So, this post is an attempt to dive deeply into the state of JavaScript development now in 2025, the way I wish I got introduced to it all.

Assumptions for reading this post

This post is aimed at someone building a fullstack web application, with a basic understanding of programming and other languages like Python, but not (yet) much experience in JavaScript development (abbreviated "JS" below).

As we go through each section, I'll focus on a decision, and answer three questions:

  1. 🤷 What's the reason this decision exists?
  2. ⚡️ What's a simple recommendation for a new greenfield project today?
  3. 🤓 Why does it actually matter? What alternatives are there?

The first question is the most important, but the other two help cater to your specific needs. When you are starting a new greenfield project, you'll have more control on the decision itself; when you join an existing one, it's usually more realistic to go with a pre-existing decision and move on, but you'll still benefit from understanding its context.

(For example, RedwoodJS made some similar decisions, but many different ones, just due to its nature as an existing project.)

Summary

Section/Decision⚡️ Recommendation for new projects🤷 🤓 Details
1. Make it work/Go to section
1.1: Development runtimenode at v22Read more
1.2: Package managerpnpmRead more
1.3: Module typeES ModuleRead more
1.4: Backend serverNothing, or fastifyRead more
1.5: Frontend web bundlerviteRead more
1.6: Frontend UIreact at v18.3Read more
2. Make it right/Go to section
2.1: Typing and TypeScriptYes, use TypeScriptRead more
2.2: Target versiones2022Read more
2.3: Lintingtypescript-eslintRead more
3. Make it fast/Go to section
3.1: FormattingprettierRead more
3.2: UI StylingTailwindCSS and FlowbiteReadmore
3.3: Client <-> server protocolGraphQLRead more
3.4: Database ORMprismaRead more
3.5: Testing frameworkJest and StorybookRead more

Decision 0.1: Editor

Before diving into JS development, you'll need to set up your editor. If you already have a favorite editor, keep using it!

But if not, Visual Studio Code has lots of good integrations with other decisions that will appear later, so you can get to effective coding faster with relatively low setup cost.

[Level 1] Make it work

In the first level, we'll examine the most basic decisions to start writing JavaScript code. The code will "work" in the sense that it will run, but it may not be the most efficient or effective to keep building on quite yet. Level 2 will help with that part instead.

Decision 1.1: Development Runtime

🤷 What's the reason this decision exists?

JavaScript actually has many options for the runtime, its version, and how to install it all. We need to make this first decision before doing anything else.

⚡️ What's a simple recommendation on a new greenfield project today?

Node v22 installed using nvm. The below instructions come from nvm and corepack:

# The following just needs to be run once globally, not per-project
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
$ nvm install 22
$ corepack enable
# Now, start a project folder my-project-2025 and use it
$ mkdir my-project-2025
$ cd my-project-2025
$ nvm use --save 22

🤔 Why does it matter? What alternatives are there?

I do rely on an assumption that we are either happily or begrudgingly choosing JS. Without that assumption, language choice for the backend is worth a post itself, and something I'll admit to not have tried to fully understand yet.

While having "one" language (ignoring the "target versions" decision in a bit!) isn't necessarily the best decision-making factor, it's certainly a helpful simplification to make when starting out, before we see other factors arise (e.g. performance).

Comic from Schematical: "Y2K 25 Years Later"

via Schematical

Within JS, several runtime options exist today (node, deno, bun). I recommend node purely from its long-established usage, even though deno was made by the same creator trying to rectify its mistakes in security and ergonomics and bun has some impressive benchmarks on speed. (With Deno 2.0 recently announced in October 2024, I think all are worth keeping an eye on for the future.)

Node has different versions (just like Python, Rust, etc.), and the packages you want to use may target a minimum requirement node version that necessitates a higher one than what your machine currently has installed... or a lower one. So, nvm helps quickly switch between versions. This also helps resolve seemingly random permission issues that may arise from otherwise installing node globally, as explained in this StackOverflow question.

The choice for node v22 in 2025 is simply because it's new enough to be officially well-supported (as the current Long Time Support edition), but not so new that there are still bugs to be ironed out.

Finally, run corepack enable and nvm use --save 22 to allow packages to specify their package managers and node versions, which will be elaborated a bit later too.

Decision 1.2: Package Manager

🤷 What's the reason this decision exists?

You'll almost never build purely from scratch, and instead usually use libraries (packages) that others have published. node ships with a package manager npm, which is both a tool and a registry, but there are many alternatives to both.

⚡️ What's a simple recommendation on a new greenfield project today?

pnpm, with both its installation steps and 2 extra commands:

$ corepack enable pnpm
$ pnpm init
$ pnpm install

🤔 Why does it matter? What alternatives are there?

In addition to npm, pnpm and yarn are other package managers with a lot of traction.

pnpm is mature enough, benefits from efficient disk usage, and has good dependency and workspace management. It installs from the NPM registry, but JSR is another alternative that's also compatible with Deno and Bun.

Regardless of which package manager you use, one overaching goal is reproducible builds no matter which computer is running your project and who might try to run it in the future. Running pnpm init and pnpm install will record the package manager and version to help further lock in a reproducible build.

Decision 1.3: Module Type

🤷 What's the reason this decision exists?

There are two sub-decision here:

  • Type of the module for my own project, as defined by a Node project's package.json
  • Type of the module I want to use/import

⚡️ What's a simple recommendation on a new greenfield project today?

ES Module for own project, by updating package.json:

{
"name": "...",
"type": "module",
...
}

and either type for importing other modules.

🤔 Why does it matter? What alternatives are there?

This article by Devlin Glasman on Modules and Bundling is a relevant read that explains the difference between CommonJS and ES Modules.

The recommendation for ES Module is for the first consideration ("my own project"), since it's the direction of the future, and can also easily import CommonJS modules.

The reverse for importing was not true for a while: CommonJS projects could not easily reuse ES Modules. And today, many packages publish as ES Modules only, with many of them linking to this disclaimer. But there seems to be some good news: if you have to use CommonJS as the module system for your project, you may be able to take advantage of a relatively recent feature in node v22 by Joyee Cheung to use them as well.

Decision 1.4: Backend Server

🤷 What's the reason this decision exists?

When someone navigates to your web application, their request needs to be served by something.

⚡️ What's a simple recommendation on a new greenfield project today?

No backend and rely on a static deploy (if no stateful backend logic is needed), or serverless with fastify (if you do need that backend logic).

🤔 Why does it matter? What alternatives are there?

When I was in college, I paid a webhost company $5 per month to simply display this website when someone navigated to fredzhao.com. That $5 reserved some compute time from a Virtual Private Server from the webhost to answer a few request a year... not very good value!

Nowadays, a simple blog like this without "state" can be hosted for free on Github Pages, as I detailed back in 2014. The site generator has since been updated from Jekyll to Gatsby, but the core has been the same: I use either my computer or Github Actions to build some pages, and then Github serves them for free by doing a static site deploy.

If a more stateful server is needed, a popular backend server is express. Since it's been around for many years, it enjoys a much bigger community. However, for starting a new project, fastify is a newer alternative that integrates better with more recent JS features like async functions, so I think it's a good option for a new project. This reddit answer gives a good summary for why fastify is great for new projects.

Finally, "serverless" is a paradigm of providing compute on-demand rather than through an always-on server. It can be much cheaper, especially with many providers having a free tier for it. You can see notes on using it with fastify on the latter's official docs here.

Decision 1.5: Frontend Web Bundler

🤷 What's the reason this decision exists?

Fullstack web applications need to run... well, on the web, in people's browsers ("frontends").

Unlike the execution of JS on the backend that has direct access to source files, frontend JS execution requires the code to somehow be delivered to those browsers. This has evolved quite a bit over time, in both the way a programmer needs to separate their modules and how those modules will actually be assembled and delivered.

⚡️ What's a simple recommendation on a new greenfield project today?

Vite is a popular choice. From its Getting Started guide:

$ pnpm add -D vite

Add two files, index.html and hello.js:

<html>
<head><script src="hello.js"></script></head>
<body>World!</body>
</html>
window.alert('Hello...');

And now you can view it bundled together via

$ pnpm vite

🤔 Why does it matter? What alternatives are there?

The earlier mentioned article by Devlin Glasman on Modules and Bundling also does a great job elaborating on the history for web bundling over time. To oversimplify it: during development, we benefit from the JS code being spread across local files and imported packages; but when we later need to deploy the application, it all needs to be combined together into much fewer files to be delivered more efficiently to web browsers.

And because browsers just run JavaScript instead of TypeScript (elaborated below), the bundler also performs transformations and transpilations.

The choice for vite is due to its speed and plugin system. I've also seen other options like webpack and esbuild, but vite seems to be the current widely preferred choice.

Decision 1.6: Frontend UI

🤷 What's the reason this decision exists?

Frontend development was once done with very separate layers over HTML and JavaScript, with the latter only directly able to do very imperative operations to the Document Object Model (DOM). Today there are lots of options to help you build out the UI very quickly, so we don't need to resort to those imperative operations.

⚡️ What's a simple recommendation on a new greenfield project today?

react at v18.3:

$ pnpm add react@18.3 react-dom@18.3

🤔 Why does it matter? What alternatives are there?

This article is a great summary of the history that web programming has gone through over the years.

react emerges as my recommendation because my professional experience has all been on it, so that's a simple and easy bias.

For someone new, I think it's also great for a huge ecosystem to get community help and learn from examples. Aside from React, there are other popular options like vue and svelte, so any option with a large community and good documentation could be worth a try.

React's v18 has been out since 2022 and v18.3 adds helpful warnings for upgrading to v19 as mentioned in the latter's upgrade guide.

[Level 2] Make it right

In this second level, we'll examine decisions that help you write better code, and build more robustly on top of the basic decisions made in Level 1.

Decision 2.1: Typing and TypeScript

🤷 What's the reason this decision exists?

JS is dynamically typed, which allows for faster code writing than statically typed languages. We could just keep writing regular ("vanilla") JS this way.

However, it's possible to use development-time tools to effectively make it a statically typed language instead, which helps reduce errors and speed up development in the long run.

⚡️ What's a simple recommendation on a new greenfield project today?

Use TypeScript at the latest version:

$ pnpm add -D typescript

Some helpful options are off by default, and need to be enabled from a configuration file called tsconfig.json. This article provides a good initial config file. I like it because it has noUncheckedIndexedAccess enabled by default, which passes my bar for a thoughtful starting point.

Now, you can manually do a type check using the terminal, or set up your editor to do it automatically on code changes. VSCode will auto-suggest a plugin; google to set it up for other editors.

$ pnpm tsc

🤔 Why does it matter? What alternatives are there?

The benefits of static typing are well-documented, and the TypeScript website has a good overview of them.

A necessary tradeoff is that static typing will reject some valid code by design, which may necessitate writing the code differently.

Why the latest version of TypeScript? Features tend to be additive, so picking whatever is the latest version should be safe. Just be sure to install at the project scope and not global scope!

Decision 2.2: Target Version

🤷 What's the reason this decision exists?

Each year, the JS language specification evolves. We need to decide on a year to target, which will affect the features we can use on both the server and the browsers that will run the code.

⚡️ What's a simple recommendation on a new greenfield project today?

Update tsconfig.json:

{
...
"target": "es2022",
...
}

ES2022 is a balance between "new-enough" language specifications, and old-enough to have enough browser support. This is similar to the decision for the Backend Server, but is more conservative and errs on running on older browsers.

🤔 Why does it matter? What alternatives are there?

JS's language specification is abbreviated ES for ECMAScript, whose namesake Wikipedia article elaborates on the history. To over-simplify a bit, we can think of these as synonymous for the purpose of the rest of this post:

  • JS
  • JavaScript
  • ES
  • ECMAScript

For a language like Python, it's both a language and an implementation: python.org defines the language specification and provides a default implementation CPython, elaborated in this Reddit thread. That allows for a more straightforward standardization process.

In contrast, JS is just a language standard, and there's no prescriptive implementation when you (or your users) open a browser or fire up a backend service. But over time, it inevitably had to evolve to fill in various gaps, like how to handle async processing (callbacks -> promises -> async functions) and what default functions exist on Array objects.

Put another way, JS has had a distributed evolution, from different engines implementing a language specification standard, that happened simultaneously with the evolution of that standard itself.

  • Runtime engines include JavaScriptCore, SpiderMonkey, V8, each of which are used in some browser frontends and server backends
  • The language specification has evolved as part of a committee called TC39. In theory, anyone can make recommendations and proposals to the committee; someone I knew from college added Array.prototype.includes!

Note that while vite helps do a lot, it doesn't handle much older browsers. If your need to handle them, you might also need to send some code to patch up those browsers using a mechanism called polyfills.

Decision 2.3: Linting

🤷 What's the reason this decision exists?

Linting is a way to enforce coding conventions, and catch errors before they happen. It's a way to help maintain clean code, and is closely related to typing.

⚡️ What's a simple recommendation on a new greenfield project today?

ESLint with typescript-eslint:

$ pnpm add -D eslint @eslint/js typescript-eslint

Similar to TypeScript, this also needs some initial configuration. Refer to its official site for a recommended configuration.

Like before, use the terminal to run it directly, or set up auto lint warnings with your editor (VSCode-suggested plugin, or google for your own browser).

$ pnpm eslint .

🤔 Why does it matter? What alternatives are there?

Linting is very closely related to typing. It helps maintain clean coding conventions. Similar to static typing, it will reject more buggy code at the tradeoff of also rejecting some benign code. This is usually still worth it!

Aside from eslint, there's also oxlint and Biome, the latter which is also mentioned in the next section.

[Level 3] Make it fast

In this third level, we'll examine decisions that help increase the rate of subsequent iteration. They may not be as immediately obvious in need as the previous levels, but will pay dividends in the long run on any projects.

Decision 3.1: Formatting

🤷 What's the reason this decision exists?

Languages like Go provide default formatters like gofmt, but JS doesn't. By keeping our code consistently indented and styled, formatters help reduce cognitive load for future collaborators -- include our future selves.

⚡️ What's a simple recommendation on a new greenfield project today?

Prettier:

$ pnpm add -D prettier

Then, add an ignore file so it won't format certain files, such as the one that pnpm auto-generated.

# Add this to a new file: .prettierignore
# Ignore files for pnpm
pnpm-lock.yaml

Finally, we can auto-format from one of two ways: using a terminal command, or from an editor's autofix whenever we save a file (VSCode will auto-prompt this; Google for specific instructions for other editors).

$ pnpm prettier -w .

🤔 Why does it matter? What alternatives are there?

An auto-formatter helps keep a consistent code style, so you can save energy from thinking about "correct" formatting to devote to the business logic instead.

Aside from prettier, there's also Biome which is up-and-coming, but let's go with a proven and opinionated tool for now.

Decision 3.2: UI Styling

🤷 What's the reason this decision exists?

Styling is a big part of building out the UI of your app. We need to pick a tool to help with this.

⚡️ What's a simple recommendation on a new greenfield project today?

TailwindCSS plus a corresponding component library like flowbite:

$ pnpm add -D tailwindcss flowbite

🤔 Why does it matter? What alternatives are there?

I'm not a frontend expert, so if you aren't one either, you'll want to save time by reusing existing style libraries!

Decision 3.3: Client <-> Server Protocol

🤷 What's the reason this decision exists?

The client and server are really two different languages, and you may need to address backwards compatibility.

⚡️ What's a simple recommendation on a new greenfield project today?

GraphQL:

$ pnpm add graphql

🤔 Why does it matter? What alternatives are there?

Similar to using TypeScript, adding more static typing helps avoid errors. Another alternative is trpc; just keep in mind that client and server are really TWO languages, and you may need to address backwards compatibility.

There's also plain REST, but GraphQL has been gaining popularity for its flexibility and type safety. Facebook developed GraphQL, while Airtable has been using REST.

Decision 3.4: Database ORM

🤷 What's the reason this decision exists?

Once the application becomes complex, we'll likely need a database to store users and their data. An object relational mapper (ORM) helps simplify writing queries to it.

⚡️ What's a simple recommendation on a new greenfield project today?

Use prisma ORM:

$ pnpm add -D prisma

🤔 Why does it matter? What alternatives are there?

This post doesn't have a recommendation for the actual database choice itself. Prisma helps you get started quickly with a local DB (sqlite) and then later change to a better production choice (MySQL, Postgres, etc.). Read about the reasoning here.

There are many ORM options for TypeScript: Hasura, TypeORM, and many more. Prisma's ORM is open source, so is a good place to start.

Decision 3.5: Testing Framework

🤷 What's the reason this decision exists?

As you work further on your project, adding automated tests will help prevent regressions.

⚡️ What's a simple recommendation on a new greenfield project today?

Storybook for UI, Jest for everything else:

$ pnpm add -D @storybook/react jest

🤔 Why does it matter? What alternatives are there?

Storybook is a great way to build out your UI components in isolation, and Jest is a popular testing framework that works well with React. There are other options like mocha and cypress, but Jest is the most widely used and has the most resources available.

Conclusion

This started as an effort to make sure that I'm not too afraid to ask "why?" in an ecosystem that I should have understood already.

knowyourmeme image: "Too Afraid to Ask" Andy

from knowyourmeme

I hope the resulting summary and deep dive has been helpful for someone new to the space too!

Overall, JS in 2025 feels like a "glorious and schizophrenic chaos". This is meant as neither praise nor critique for the language, but more a reflection of how JS has evolved in a distributed way over the years. This has created some tradeoffs and complexities.

  • The "Glorious": JS can run everywhere! Many other languages can be transpiled to run in browsers, but JS runs natively. So it can feel glorious to build an entire app using the same language
  • "Schizophrenic" is an important reminder that working with fullstack JS means writing in two languages, one for the frontend and one for the backend. They just happen to have the same file extensions and use the same syntax!
  • Finally, "Chaos" reflects the various configurations and decisions that arise from its rapid development over time. What feels like good recommendations today may very much become outdated by 2026

So I hope the reasoning for understanding each decision prepares ourselves for this ever-changing landscape, and retain a bit of sanity through that chaos. A bit like Loki at the end of Season 2, grasping at various multiverse timelines to keep it all together.

loki timelines

References and Inspirations

Thanks to Michael Mitchell for reading through an earlier draft and giving feedback.

Many articles I used for reference have already been linked above. If you enjoyed this post, please also check out some of the inspirations that helped me write it!