Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 082 / 100

The JS/TS toolchain in 2026: what developers should know

What I use and why.

Expona Admin6 min read

The JS/TS toolchain in 2026: what developers should know

After years of churn in the JavaScript ecosystem, the tooling landscape has finally stabilized around a set of proven, production-tested patterns. We're no longer experimenting with whether Webpack, Rollup, or Parcel is "the answer"—we've converged on practical choices based on the actual problem we're solving. This guide covers what we use in production and why, so you can make informed decisions when setting up projects or evaluating team standards.

Runtimes: Node for production, Bun and Deno where they fit

Node.js remains the default choice for production workloads across e-commerce, SaaS, ride-hailing, and trading platforms. Its maturity, ecosystem density, and operational familiarity make it the safe default. We deploy Node servers handling millions of requests daily without surprises.

That said, the runtime landscape has diversified. Bun offers compelling performance gains for CPU-bound tasks and has become our go-to for local development and tooling scripts. Its native TypeScript support eliminates the ts-node bootstrap overhead, and its bundler is genuinely fast for quick builds. We've used Bun for internal CLI tools, database migration runners, and build orchestration where startup time matters.

Deno shines in specific niches: serverless edge runtimes (Cloudflare Workers, Vercel Edge), sandboxed script execution, and greenfield projects where you want explicit permissions and no package.json chaos. Its standard library is cleaner than npm's fragmentation, and explicit imports force clarity. However, for traditional backend services, the ecosystem is still catching up.

In practice, we run Node in production, prototype with Bun locally, and deploy Deno code to edge environments where its lightweight nature and instant startup are advantages.

typescript
// Local development script with Bun
import { parseArgs } from "util";
import { readdir } from "fs/promises";

const { values } = parseArgs({
  options: {
    pattern: { type: "string", short: "p" },
  },
});

const files = await readdir("./src");
const filtered = files.filter(f => f.includes(values.pattern || ""));
console.log(filtered);

Running this with `bun run script.ts` is instant—no compilation step visible to the user. With Node, you'd reach for ts-node or tsx, which add measurable overhead in quick tooling.

Bundling: Vite for apps, tsup or tsdown for libraries

Vite has become the standard for frontend and full-stack applications. Its dev server startup is orders of magnitude faster than Webpack, and HMR (hot module replacement) actually feels hot now. We use Vite across React, Vue, and Svelte projects with consistent, minimal configuration.

For libraries, Vite isn't the right fit—you need a bundler optimized for dual ESM/CJS output, tree-shaking, and keeping bundle size tight. tsup remains our workhorse. It's zero-config by default, outputs both ESM and CommonJS, and handles TypeScript declaration generation. For a typical NestJS shared library or React component library, tsup "just works."

tsdown is newer but worth watching; it's written in Rust and offers faster builds on large codebases. If your library has hundreds of modules or you're bundling for multiple target environments, the speed gains matter. We've started adopting it for our largest shared packages.

Use case Tool Why
Frontend app (React, Vue, Svelte) Vite Fast dev server, excellent DX, production-tested
Library with dual output tsup Zero-config, ESM + CJS, .d.ts generation
Large library, performance critical tsdown Rust-powered, faster builds on big codebases
Backend/API (NestJS, Express) tsc (direct compilation) No bundling needed; just type-check and emit JS

A typical tsup configuration is refreshingly minimal:

typescript
// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
});

That generates both ESM and CommonJS, TypeScript declarations, and source maps. No plugins, no wiring.

Lint and format: ESLint flat config with Prettier, or Biome for speed

ESLint's flat config (introduced in v8, now standard) is a major improvement over the old eslintrc chaos. Flat config is JavaScript-based, composable, and scales better across monorepos. We pair it with Prettier for formatting, which eliminates the "tabs vs. spaces" debates and lets ESLint focus on rules that matter: logic errors, unused variables, import ordering.

A modern setup looks like this:

typescript
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";

export default [
  {
    ignores: ["dist/**", "node_modules/**", "build/**"],
  },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier, // Disables ESLint rules that Prettier handles
  {
    files: ["src/**/*.ts"],
    rules: {
      "no-console": "warn",
      "prefer-const": "error",
    },
  },
];

Biome is an emerging alternative that bundles linting, formatting, and basic type checking into a single, incredibly fast Rust binary. It's production-ready for new projects and appeals to teams that want minimal configuration overhead. The trade-off: less mature than ESLint + Prettier combined, and fewer community plugins. For teams on established codebases with heavy ESLint customization, the switch isn't worth the effort. For greenfield projects or small teams, Biome cuts setup time significantly.

We're using Biome in newer projects and keeping ESLint + Prettier in systems where we've invested in custom rules and shared configs across teams.

Type checking: tsc --noEmit in CI

Type checking in CI is non-negotiable in TypeScript projects. We run `tsc --noEmit` (or equivalent in Biome) as part of the pull request checks, separate from bundling. This catches type errors that bundlers might miss or report unclearly.

The flag `--noEmit` tells TypeScript to validate types without producing JavaScript files—it's purely a check. In CI, this runs fast and catches issues before merge.

typescript
// package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "vite build",
    "lint": "eslint ."
  }
}

Your CI pipeline runs all three independently. Type checking catches type errors. Linting catches code quality and logic issues. Building verifies the output is valid. They complement, not replace, each other.

For monorepos using Nx or Turbo, type checking runs on changed packages only, keeping CI time reasonable as the codebase grows.

Common mistakes

  • Relying on bundler type checking. Vite and esbuild skip type checking by default for speed. Don't assume your bundler caught that type error—run tsc in CI explicitly.
  • Over-configuring Prettier. Prettier's philosophy is "no configuration." Adding dozens of options defeats the purpose. Use defaults, and if you absolutely need tweaks, keep them minimal.
  • Mixing ESLint and Prettier without coordination. Conflicting rules between ESLint and Prettier waste CI time on false positives. Use `eslint-config-prettier` to disable formatting rules in ESLint.
  • Using Bun for production without pinning versions. Bun is fast for local development, but Node remains more stable for production workloads. Test thoroughly if you deploy Bun to prod.
  • Ignoring tsup's output. tsup handles .d.ts generation automatically, but only if you've configured `dts: true`. Check your published npm package actually exports types before shipping.

Key takeaways

  • Use Node for production. Bun and Deno are excellent for specific use cases (local tooling, edge functions), but Node's ecosystem and operational maturity remain unmatched.
  • Vite for apps, tsup for libraries. Each tool solves its problem well; don't force one tool to do both.
  • Pair ESLint flat config with Prettier, or adopt Biome for simplicity. Either approach works; choose based on your team's existing investment and comfort level.
  • Type check in CI with tsc --noEmit. Don't rely on bundlers to catch type errors; use TypeScript's compiler as a dedicated check step.
  • Standardize within your team, document once, move on. The "perfect" toolchain matters less than consistency and clarity across your projects.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles