


Node 22 LTS, pnpm, Vite, Vitest, ESLint flat, Zod, Turborepo: the stack I default to in 2026 and why boring beats revolutionary.
Choosing your Node.js + TypeScript stack in 2026 is less a matter of fashion than of cumulative DX over 2 years. Here's the default stack I use and why.
DX (Developer Experience) isn't comfort, it's investment. A stack that costs you 15 minutes a day waiting for builds or decoding cryptic errors is 60 hours per year per developer. On a team of 4, that's a half-FTE lost.
This article condenses the stack I default to in 2026, with the trade-offs that led me there. Not "you must use this" — criteria to decide.
Three contenders in 2026, context by context:
| Criterion | Node 22 LTS | Bun 1.x | Deno 2.x |
|---|---|---|---|
| Production maturity | ★★★★★ | ★★★ | ★★★★ |
| Performance | ★★★ | ★★★★★ | ★★★★ |
| npm ecosystem | ★★★★★ | ★★★★ | ★★★★ |
| Integrated tooling | ★★ | ★★★★★ | ★★★★★ |
| Native TypeScript | With --experimental | Yes | Yes |
| Hosting adoption | All platforms | Limited | Growing |
My default: Node 22 LTS. Reasons:
Bun: for highly perf-critical workloads (parsers, transformers, mass tests). 4x faster startup changes CI.
Deno: for edge / short serverless projects. Permission model and native HTTPS imports are elegant, but ecosystem friction weighs.
For a web app: Vite, period. DX is unmatched: sub-1-second startup, instant HMR, native TS + JSX, rich plugin ecosystem.
For a backend service: esbuild, because you don't need Vite's dev server. esbuild in watch mode rebundles in <200ms, perfect for fast tests.
To publish an npm package: tsup (esbuild wrapper) or tsdown. Generates dual ESM/CJS + types in one command.
| 1 | { |
| 2 | "scripts": { |
| 3 | "build": "tsup src/index.ts --dts --format esm,cjs", |
| 4 | "dev": "tsup src/index.ts --watch --dts --format esm,cjs" |
| 5 | } |
| 6 | } |
TypeScript in strict mode plus a few extra flags:
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "strict": true, |
| 4 | "noUncheckedIndexedAccess": true, |
| 5 | "noImplicitOverride": true, |
| 6 | "noPropertyAccessFromIndexSignature": true, |
| 7 | "exactOptionalPropertyTypes": true, |
| 8 | "noFallthroughCasesInSwitch": true, |
| 9 | "forceConsistentCasingInFileNames": true, |
| 10 | "isolatedModules": true, |
| 11 | "verbatimModuleSyntax": true, |
| 12 | "target": "ES2022", |
| 13 | "module": "NodeNext", |
| 14 | "moduleResolution": "NodeNext" |
| 15 | } |
| 16 | } |
noUncheckedIndexedAccess: arr[0] becomes T | undefined, forcing empty-case handlingexactOptionalPropertyTypes: { a?: string } no longer accepts explicit undefined, only absenceisolatedModules + verbatimModuleSyntax: perfect compatibility with esbuild/ViteThese flags cost time upfront but eliminate an entire class of bugs. On Nexus, enabling noUncheckedIndexedAccess revealed 17 latent bugs in a one-day refactor.
TypeScript validates at compile time. To validate at runtime (HTTP input, queue message, external payload), a runtime schema is essential.
Zod: de facto standard, massive ecosystem, integration with tRPC, OpenAPI, etc.
Valibot: 7x lighter, ideal for client bundles. API very close to Zod.
| 1 | import { z } from "zod"; |
| 2 | |
| 3 | export const depositSchema = z.object({ |
| 4 | amount: z.number().int().positive().max(10_000_000), |
| 5 | currency: z.enum(["XOF", "USD", "EUR"]), |
| 6 | gateway: z.enum(["mtn_momo", "orange_money", "stripe"]), |
| 7 | reference: z.string().min(3).max(64), |
| 8 | }); |
| 9 | |
| 10 | export type Deposit = z.infer<typeof depositSchema>; |
Rule: all data coming from outside passes through a runtime schema. No blind as Deposit. No "it comes from the frontend, it's OK".
ESLint v9 in flat config (eslint.config.js) is now standard.
| 1 | import tseslint from "typescript-eslint"; |
| 2 | import unicorn from "eslint-plugin-unicorn"; |
| 3 | |
| 4 | export default tseslint.config( |
| 5 | ...tseslint.configs.strictTypeChecked, |
| 6 | ...tseslint.configs.stylisticTypeChecked, |
| 7 | unicorn.configs.recommended, |
| 8 | { |
| 9 | languageOptions: { |
| 10 | parserOptions: { |
| 11 | projectService: true, |
| 12 | tsconfigRootDir: import.meta.dirname, |
| 13 | }, |
| 14 | }, |
| 15 | rules: { |
| 16 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], |
| 17 | "@typescript-eslint/consistent-type-imports": "error", |
| 18 | "no-console": ["warn", { allow: ["warn", "error"] }], |
| 19 | }, |
| 20 | }, |
| 21 | ); |
strictTypeChecked adds rules requiring type analysis (slow but valuable): no-floating-promises, no-misused-promises, require-await. These three alone catch 80% of typical async bugs.
Prettier remains the mature standard. Its plugin ecosystem (Tailwind, etc.) is unmatched.
Biome is 10–30x faster. On big monorepos, life-changing. Its linter (Biome includes its own) is still behind ESLint but catching up fast.
My 2026 default: Biome for new projects, Prettier+ESLint for existing. Biome migration is work but the speed gain is tangible.
Vitest: Jest-compatible, native ESM, ultra-fast watch, snapshot, mocking, v8 coverage. The modern Swiss Army knife.
Bun test: integrated with Bun runtime, instant startup. Excellent if you already run Bun.
node:test: native Node 20+ module, no dependency. Enough for simple tests but ecosystem is thin.
| 1 | import { describe, it, expect, vi } from "vitest"; |
| 2 | import { PaymentRouter } from "./PaymentRouter"; |
| 3 | |
| 4 | describe("PaymentRouter", () => { |
| 5 | it("resolves the right gateway for a method", () => { |
| 6 | const router = new PaymentRouter(); |
| 7 | const fakeGateway = { name: "mtn", supports: ["mobile_money_mtn"] }; |
| 8 | router.register(fakeGateway as never); |
| 9 | expect(router.resolve("mobile_money_mtn")).toBe(fakeGateway); |
| 10 | }); |
| 11 | |
| 12 | it("throws on unregistered method", () => { |
| 13 | const router = new PaymentRouter(); |
| 14 | expect(() => router.resolve("card")).toThrow(/no gateway/i); |
| 15 | }); |
| 16 | }); |
pnpm by default. Three reasons:
node_modules (shared hardlinked store)npm stays OK for very small projects or a well-structured monorepo with npm workspaces. Yarn berry is technically excellent but learning curve and smaller ecosystem make it less justifiable in 2026.
For a monorepo of 3 to 15 packages: Turborepo. Simple config, Vercel remote cache, perfectly integrated with pnpm.
For an enterprise monorepo with 30+ packages and strong architectural constraints: Nx. More powerful, more complex, steeper curve.
| 1 | { |
| 2 | "$schema": "https://turbo.build/schema.json", |
| 3 | "tasks": { |
| 4 | "build": { |
| 5 | "dependsOn": ["^build"], |
| 6 | "outputs": ["dist/**", ".next/**"] |
| 7 | }, |
| 8 | "lint": { "dependsOn": ["^build"] }, |
| 9 | "test": { "dependsOn": ["^build"] }, |
| 10 | "dev": { "cache": false, "persistent": true } |
| 11 | } |
| 12 | } |
GitHub Actions became the practical standard in 2026. GitLab CI stays excellent if you're already on GitLab.
For a TypeScript/Node project:
| 1 | name: CI |
| 2 | on: [push, pull_request] |
| 3 | jobs: |
| 4 | ci: |
| 5 | runs-on: ubuntu-latest |
| 6 | steps: |
| 7 | - uses: actions/checkout@v4 |
| 8 | - uses: pnpm/action-setup@v3 |
| 9 | with: { version: 9 } |
| 10 | - uses: actions/setup-node@v4 |
| 11 | with: { node-version: 22, cache: pnpm } |
| 12 | - run: pnpm install --frozen-lockfile |
| 13 | - run: pnpm typecheck |
| 14 | - run: pnpm lint |
| 15 | - run: pnpm test |
| 16 | - run: pnpm build |
Well-done pnpm cache: 2 minutes vs 6 without on an average project.
My 2026 stack for a new Node/TS project:
| Category | Pick |
|---|---|
| Runtime | Node 22 LTS |
| Package manager | pnpm |
| TypeScript | strict + extra flags |
| Runtime validation | Zod |
| Linter | ESLint flat + typescript-eslint strict |
| Formatter | Biome (new) or Prettier (existing) |
| Tests | Vitest |
| Web app bundler | Vite |
| Service bundler | esbuild + tsup for packages |
| Monorepo | Turborepo + pnpm workspaces |
| CI | GitHub Actions |
This stack is boring, and that's exactly the point. No component is revolutionary. All are mature, documented, hireable. Time not spent on the stack is time spent on the product.
| Pitfall | Symptom | Fix |
|---|---|---|
| Exotic stack to look cool | Degraded DX, hard hiring | Default choice unless precise justification |
| Non-strict TypeScript | Frequent runtime bugs | strict: true + extra flags |
| No runtime validation | Crash on unexpected input | Zod or Valibot on all inputs |
| Too lax linter | Normalized dubious patterns | typescript-eslint strict + async rules |
| No CI cache | Slow builds demoralize | pnpm cache + Turbo cache |
| Premature monorepo | Complexity without benefit | Single package up to 2-3 apps |
| Targetless tests | Symbolic coverage | Tests on business logic, not framework |
The best 2026 stack isn't the newest. It's the one that saves the most minutes per day without cognitive overhead.
My selection criteria, in order:
A team of 4 developers with the right stack ships 30–50% faster than with a poorly chosen stack. The math is rarely spelled out but it's massive.
If starting today, take the default stack. Customize only when a precise, quantified need justifies it. The rest is cosmetics.
If this topic feels close to a real product problem, I can help on diagnosis, architecture, backend, interface and automations that make a platform usable in production.
Reader reactions
No comment yet
Be the first to share your reaction.