Developer tooling for TypeScript and React

Budget: 2-3 days.

Why this matters

Coming from Python you already have a stable mental model of the toolchain: uv manages dependencies, ruff lints and formats, mypy or pyright type-checks, pylsp powers the editor, and pyproject.toml holds it all together. The JavaScript world has the same roles, but the tools are split differently and the ecosystem moves faster. Knowing the map up front means you spend time learning React, not fighting configs.

Python -> JS/TS map

RolePythonJavaScript / TypeScript
RuntimeCPythonNode.js (or Bun)
Package registryPyPInpm registry (registry.npmjs.org)
Package manageruv, pip, poetrynpm, pnpm, bun
Lockfileuv.lock, poetry.lockpackage-lock.json, pnpm-lock.yaml, bun.lockb
Project configpyproject.tomlpackage.json (+ tsconfig.json for TS)
Linterruff, pylint, flake8ESLint, Biome
Formatterruff format, blackPrettier, Biome
Type checkermypy, pyrighttsc (the TypeScript compiler itself)
Editor LSPpylsp, ruff-lsp, pyrighttsserver (built into VS Code)
Test runnerpytestVitest, Jest
Build tool / bundler(none, Python is interpreted)Vite, esbuild, Rollup, webpack

What's actually different

  • The compiler is the type checker. tsc plays the role of mypy. It does not produce JavaScript at runtime in modern setups; Vite uses esbuild/swc for that. You run tsc --noEmit purely to type-check, the same way you run mypy ..
  • There is no Python equivalent of bundlers. Browsers can't import thousands of small files efficiently, so a build tool concatenates and tree-shakes your code. Vite is the current default. You already use it in frontend/.
  • package.json is pyproject.toml plus a Makefile. Dependencies live under dependencies and devDependencies, and the scripts block is where you keep dev, build, lint, test. Run them with npm run <name>.
  • Lint and format are still separate by default. ESLint lints, Prettier formats. Biome is the Rust-based newcomer that does both, and it's the closest thing to ruff in spirit (single tool, fast, opinionated). For a new project today, Biome is reasonable; for an existing project with ESLint already wired up (like yours), staying on ESLint + adding Prettier is the path of least resistance.
  • Package manager choice matters less than in Python. npm is fine. pnpm is faster and uses a content-addressable store (closest to uv's vibe). bun is the all-in-one upstart. Pick one and stick with it; your frontend/ already uses npm based on package-lock.json.
  • tsconfig.json is where TypeScript's strictness lives. Turn on strict: true from day one. That single flag enables strictNullChecks and friends, which is where most of TypeScript's value comes from.

What to learn

TopicTime
package.json anatomy: dependencies, devDependencies, scripts, the type: module field0.5 day
npm commands you'll actually use: install, run, ci, update, outdated0.5 day
tsconfig.json: strict, target, module, moduleResolution, paths0.5 day
ESLint v9 flat config: how eslint.config.js works, how plugins compose0.5 day
Prettier (or Biome) setup and editor-on-save formatting0.5 day
Vite: dev server, vite build, plugins, env vars (import.meta.env)0.5 day
Vitest: writing a first test, running in watch mode0.5 day
VS Code TypeScript integration: go-to-def, rename symbol, organize imports0.5 day

Resources

Starter project

Before you touch the FastVRP frontend, scaffold a small throwaway app from scratch. The goal is to feel each tool's role on its own, with no legacy config in the way.

Check your Node and npm

node -v   # v20 or newer
npm -v    # v10 or newer

If either is missing or old, install Node from nodejs.org or via nvm. npm ships with Node, so you only install one thing.

Scaffold with the shadcn Vite starter

One command gives you React + TypeScript + Vite + Tailwind v4 + shadcn, with path aliases already wired:

npx shadcn@latest init -t vite
npm install

That saves you from manually configuring Tailwind, the @ alias in both tsconfig.json and vite.config.ts, and the shadcn init. You could do each step by hand (and it's worth reading the shadcn Vite guide once to see what the starter is doing), but for a throwaway app the starter is the cleanest path.

Add a component or two when you need them:

npx shadcn@latest add button input checkbox

The components land in src/components/ui/. Read them. They are short, typed, and built on Radix primitives. This is the easiest way to see what "good" React + TS + Tailwind code looks like.

Add Biome

The shadcn starter does not include a linter or formatter, so layer Biome on top:

npm install --save-dev --save-exact @biomejs/biome
npx biome init

Then add scripts to package.json:

"scripts": {
  "format": "biome format --write .",
  "lint": "biome lint .",
  "check": "biome check --write ."
}

check runs lint and format together and fixes what it safely can. Install the Biome VS Code extension and turn on "format on save" so you stop thinking about formatting.

Check the .gitignore

The shadcn Vite starter ships with a .gitignore. Make sure it covers at least:

node_modules
dist
.env
.env.local
.DS_Store

node_modules and dist are the big two: never commit your dependency tree or build output. .env* keeps secrets out of git. .DS_Store keeps macOS Finder droppings out of your commits.

Run Biome in CI

Add .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx biome ci .

biome ci is the read-only flavor: it lints, format-checks, and import-sorts, and fails the job if anything is off. It does not write files. That's the right behavior for CI: a PR that drifts from your rules should fail, not silently auto-fix.

If you want type-checking on every push, add a second step:

      - run: npx tsc --noEmit

What to build

A todo list with three features:

  • Add, remove, and toggle items, using shadcn's Button, Input, and Checkbox for the UI.
  • Persist to localStorage so a refresh keeps the list.
  • One typed shape, type Todo = { id: string; text: string; done: boolean }, threaded through state, props, and the storage helper.

Style only with Tailwind utility classes. No router, no state library, no backend. The point is to use useState, useEffect, typed props, and utility-first styling on something that runs end to end.

What you'll learn

  • How tsc --noEmit catches a typo that would have crashed at runtime.
  • What Biome flags out of the box, and which rules you actually want.
  • How npm run dev and npm run build differ in what they produce.
  • How Tailwind classes compose, and when to extract a component instead of duplicating a long class string.
  • How shadcn components are wired: Radix primitives, cn() for class merging, variants via class-variance-authority.

Throw it away when you're done. The reps are the point.

Exercise

Audit frontend/ and write down what role each tool currently plays:

  • package.json: dependencies, dev dependencies, scripts.
  • package-lock.json: lockfile.
  • eslint.config.js: lint rules.
  • vite.config.js: build and dev server config.
  • (Missing) tsconfig.json: add this when you start chapter 1's TypeScript port.
  • (Missing) prettier config or Biome: add one before the TS port so formatting stays consistent.
  • (Missing) Vitest: add when you write the first test.

Then run each script once (npm run dev, npm run lint, npm run build) and read the output. The point is to know what each tool actually does, not to change anything yet.

Notes