How to create a practical Vite + React + TypeScript dev environment

How to create a practical Vite + React + TypeScript dev environment

Vite, React, and TypeScript combine speed, stability, and strong typing to create a modern, scalable development environment that stays fast as your project grows.

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]

Was this helpful?

Thanks for your feedback!

Leave a comment

Your email address will not be published. Required fields are marked *