Vite emerged to fix the “slow feedback loop” that bundler-heavy dev servers imposed on React projects. Instead of pre-bundling everything, Vite serves native ESM in dev and only bundles for production, which made hot module replacement (HMR) feel instantaneous. Over time it standardized around Rollup for production builds, added first-class plugin ergonomics, and became the default “start here” path recommended by the React docs for hand-rolled apps. With React 19 stable and TypeScript 5.9, the trio offers fast iteration, modern syntax, and strong types—without ceremony.
flowchart LR A[Source files: .tsx/.ts/.css] -->|Native ESM| B(Vite dev server) B -->|Instant HMR| C[Browser] A -->|vite build| D(Rollup bundling) D --> E[Optimized assets for prod]
As you’ll see below, the defaults now carry you far; you only add tooling when it clearly pays for itself.
Prerequisites (keep these current):
Install an LTS Node that Vite supports. As of today, Vite requires Node 20.19+ or 22.12+. Use a modern package manager (npm, pnpm, yarn, bun) and a recent VS Code. We’ll assume npm for commands. (vitejs)
Scaffold a React + TypeScript app (with the fast SWC path)
Use Vite’s official scaffolder to generate a React + TS template that uses SWC for ultra-fast transforms in dev.
# Create in a new folder interactively
npm create vite@latest
# Or one-liner with explicit template
npm create vite@latest my-app -- --template react-swc-ts
cd my-app
npm install
npm run dev
Vite will spin up on http://localhost:5173 and HMR should feel instant even on larger modules. The react-swc
templates are maintained in the Vite org and use the official @vitejs/plugin-react-swc
, which replaces Babel during development.
Project anatomy you’ll see
You’ll get a src/
directory with a minimal React 19 app, index.html
at the project root as the entry (Vite treats it as source and wires ESM for you), and three scripts in package.json
: dev
, build
, preview
. Keep index.html
front-and-center; that is Vite’s design.
my-app/
index.html # entry served by Vite in dev
package.json # dev/build/preview scripts
tsconfig.json # compiler options for TypeScript
vite.config.ts # Vite configuration (plugins, aliases)
src/
main.tsx # app bootstrap
App.tsx # sample component
assets/ # static assets imported via ESM
Pin the “modern TS for bundlers” config
TypeScript 5.9 simplified tsc --init
and continues to recommend modern module resolution strategies. For Vite apps where TypeScript does not emit JS (Vite/SWC handles transforms), moduleResolution: "bundler"
gives friction-free path resolution that matches bundlers. Keep "strict": true
and "jsx": "react-jsx"
for React 19.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"allowJs": false,
"types": ["vite/client", "vitest/globals"] // add Vitest later
},
"include": ["src"]
}
If you publish a library, prefer nodenext
to ensure your published d.ts stay portable across consumer configs; for apps, bundler
is usually the smoother choice.
Vite config: keep it minimal, add React SWC
Start with the official React-SWC plugin. Add aliases and testing config when you need them.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src"
}
},
// You can add test config here (see the Testing section)
});
The SWC plugin speeds cold starts and HMR; in production, Vite builds with Rollup (and uses esbuild/SWC where appropriate).
React 19: use the current runtime and patterns
The Vite templates target the automatic JSX runtime (react-jsx
) and fetch React 19 from npm. You can start using 19’s improvements (e.g., refined Actions & form handling, new APIs) without custom build steps. If upgrading an older app, consult the official React 19 upgrade notes before toggling major versions.
// src/App.tsx
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<main>
<h1>Vite + React 19 + TypeScript</h1>
<button onClick={() => setCount((n) => n + 1)}>
Count: {count}
</button>
</main>
);
}
Linting with ESLint Flat Config + typescript-eslint
ESLint’s Flat Config is now the default path. The typescript-eslint
project ships helpers and “recommended” presets that work with ESLint 9. Add Prettier for formatting if you like, but keep linting and formatting concerns separate.
npm i -D eslint @eslint/js typescript-eslint prettier
// eslint.config.mjs
import { defineConfig } from "eslint";
import tseslint from "typescript-eslint";
import js from "@eslint/js";
export default defineConfig([
js.configs.recommended,
...tseslint.configs.recommended, // parser + TS rules
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parserOptions: {
projectService: true // typed linting when tsconfig is present
}
},
rules: {
// sensible React+TS tweaks go here
"no-console": ["warn", { allow: ["warn", "error"] }]
}
},
{
ignores: ["dist", "coverage"]
}
]);
Run linting and formatting:
npx eslint . --max-warnings=0
npx prettier . -w
Testing: Vitest for unit, Playwright for E2E
Vitest is Vite-native and reuses your Vite config and plugins. As of now, the latest stable tag is Vitest 3.2.x, with 4.0 in beta; use the latest stable unless a beta feature unblocks you. Playwright remains a reliable E2E harness with npx playwright install
to fetch browsers.
npm i -D vitest @vitest/coverage-v8 jsdom
// vite.config.ts (add this block)
export default defineConfig({
// ...
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
coverage: { provider: "v8", reporter: ["text", "html"] }
}
});
// src/test/setup.ts
import "@testing-library/jest-dom";
// src/App.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import App from "./../App";
it("increments", () => {
render(<App />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByText(/Count: 1/)).toBeInTheDocument();
});
# End-to-end
npm init playwright@latest
npx playwright test
For Playwright updates, prefer @latest
and re-install browsers when bumping major minors.
Environment variables that won’t surprise you
Vite exposes only variables prefixed with VITE_
to client code. Keep secrets server-side. Provide a .env
for local defaults and .env.example
for onboarding; Vite merges mode-specific files like .env.development
.
# .env
VITE_API_URL=https://api.example.com
// src/lib/api.ts
export const API_URL = import.meta.env.VITE_API_URL as string;
Production build, preview, and static hosting
Build and locally preview the production bundle:
npm run build
npm run preview
Vite uses Rollup to output optimized static assets. You can deploy the dist/
folder to any static host or combine it with a server (Node, edge runtimes) to serve APIs separately.
sequenceDiagram participant Dev as Developer participant Vite as Vite (dev) participant Rollup as Rollup (build) participant Host as Static Host / CDN Dev->>Vite: npm run dev Vite-->>Dev: HMR + ESM Dev->>Rollup: npm run build Rollup-->>Dev: dist/ assets Dev->>Host: Upload dist/ Host-->>Users: Cacheable static files

Sensible package.json scripts
Keep scripts discoverable and CI-friendly.
{
"scripts": {
"dev": "vite",
"typecheck": "tsc --noEmit",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier . -w",
"test": "vitest run",
"test:ui": "vitest"
}
}
Keeping “latest” actually latest (without breaking yourself)
- Vite: follow the official release notes; Vite 7 is current and removes some long-deprecated bits. Review migration notes before major bumps.
- React: React 19 is stable—use it by default in new apps.
- TypeScript: 5.9 is latest and focuses on quality-of-life and init simplification. Pin ranges loosely (
^5.9.0
) but keep an eye on release notes. - Vitest / Playwright: prefer latest stable tags; only opt into betas with intent.
JavaScript tooling evolves quickly; occasionally, ecosystem packages publish compromised versions. Favor official templates, pin to known-good versions, and skim release notes before mass updates—especially for lint/format packages that run on install.
A practical baseline you can reuse
You now have a reproducible baseline: Vite for speed and DX, React 19 for UI, and TypeScript 5.9 for types, plus linting, tests, and environment handling that won’t fight your build. From here, layer in routing, data fetching, and UI kits. When requirements grow, extend Vite via plugins, not ad-hoc scripts—your feedback loop will stay fast as the project scales.
References
- Vite: Getting Started, features, templates, Node requirements, and build model. [vitejs]
- Vite 7 announcement and release cadence. [vitejs]
- React docs: “Build a React app from scratch” and React 19 stable blog. [React]
@vitejs/plugin-react-swc
docs and NPM metadata. [vitejs]- TypeScript 5.9 release notes and TSConfig reference (including
moduleResolution
). [TypeScript] - Vitest docs and current stability (latest vs beta). [vitest.dev]
- Playwright intro and common install/update guidance. [Playwright]