Developer Experience
Purpose
Make every developer faster on day one. Misconfiguration that lingers wastes hours per week; the right baseline removes a whole class of "works on my machine" problems.
Universal — pre-commit hooks, strict type-checker config, path aliases, typed env validation, IDE recommendations, and CONTRIBUTING.md are baseline DX for any frontend stack.
Procedure
Linter + formatter baseline
- Lint for correctness/a11y/hooks/import hygiene; format mechanically so style is never a review topic
- Integrate the two so the formatter turns off the linter's conflicting stylistic rules
- Format on save in the editor
- Add
.editorconfigfor cross-editor consistency (indent, line endings) — not everyone uses the same editor - (ESLint / Prettier — see Implementation)
Pre-commit hooks that run lint + format on staged files only (fast)
- Install a git pre-commit hook that runs lint + format
- Scope it to staged files only so the hook stays fast and never blocks on the whole tree
- Add a
commit-msghook (commitlint) to enforce Conventional Commits — this is what later enables automated changelogs / semantic versioning - Hooks are a fast local convenience, not enforcement — they're skippable with
--no-verifyand don't run in CI. CI must re-run lint / type-check / test as the real gate (seecicd-pipeline). Keep hooks fast or they get bypassed - (husky + lint-staged + commitlint — see Implementation)
2b. Pin the toolchain for reproducible installs
- Pin Node (
.nvmrc/engines.node) and the package manager (packageManagerfield + Corepack) so everyone runs identical versions — most "works on my machine" bugs are a version mismatch - Commit the lockfile; use
npm ci(notnpm install) in CI and onboarding for reproducible installs; never mix package managers (one lockfile)
Enable the strict type-checker flags that catch a class of bugs at compile time
- Turn on the strict baseline, plus the flags for uninitialized index access (
array[i]may beundefined) andundefined-vs-missing-key distinction - These together catch a huge class of subtle bugs at compile time instead of at runtime
- Don't put project-wide
tsc --noEmitin lint-staged — type errors are cross-file, but lint-staged passes only staged files (so it misses errors, or you slow the hook by checking everything). Runtscin CI (or apre-pushhook); keep per-file lint in pre-commit - (TS
strict/noUncheckedIndexedAccess/exactOptionalPropertyTypes— see Implementation)
- Turn on the strict baseline, plus the flags for uninitialized index access (
Path alias to eliminate
../../chains- Configure a path alias (e.g.
@/→ source root) so imports are stable regardless of file depth - Eliminate
../../../import chains - Audit:
grep -rE "from ['\"]\\.\\./\\.\\./" src/→ 0 - (tsconfig paths — see Implementation)
- Configure a path alias (e.g.
Validate env via a typed schema imported at build so misconfig fails the build, not production
- Define a typed schema for server and client env, and import it where it runs at build time so a missing/malformed var fails the build rather than surfacing in production at runtime
- Reference the typed accessor instead of raw
process.env.X(which isstring | undefined) - Gotcha: list every key explicitly in the runtime-values map — a strict schema requires it, and missing keys cause type errors (the #1 adopter gotcha)
- Split server / client when variable names are sensitive: a single-file schema can leak server-variable names into the client bundle; split into separate server/client modules when the names themselves must not ship to the browser
- Commit a
.env.examplelisting every required key (no values) so onboarding and the typed schema stay in sync - (t3-env — see Implementation)
Fast dev server / HMR
- Use the fastest available dev server / hot-module-reload path for the stack
- Measure: dev server cold start, file save → reload latency
- (Turbopack — see Implementation)
Commit IDE recommended-extensions so contributors are auto-prompted
- Commit a recommended-extensions manifest listing the core dev extensions (linter, formatter, CSS IntelliSense, git tooling, inline error display)
- The editor then auto-prompts new contributors to install them
- (.vscode/extensions.json — see Implementation)
CONTRIBUTING.md
- 30-minute onboarding target: clone → install → run dev → first PR
- Include: required tools, env setup, branch naming, PR checklist
Completion Criteria
- Pre-commit hook fires on
git commitand auto-lints (staged files only) -
commit-msghook enforces Conventional Commits - Node + package manager pinned (
.nvmrc/engines/packageManager); lockfile committed - TypeScript strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes enabled
- Project-wide
tscruns in CI / pre-push (not in per-file lint-staged) - Path alias
@/works, no../../import chains - Missing env var crashes the build (not just runtime);
.env.examplelists required keys - New team member can clone → dev server running in ≤ 30 min following CONTRIBUTING.md
-
.editorconfig+.vscode/extensions.jsonpresent
Output
- Linter + formatter config:
.eslintrc.{json,cjs}+.prettierrc+eslint-config-prettier+.editorconfig - Pre-commit hook:
.husky/pre-commit+lint-stagedconfig inpackage.json;.husky/commit-msg+commitlint.config.{js,ts} - Toolchain pin:
.nvmrc+engines.node+packageManagerfield; committed lockfile - Env example:
.env.examplelisting every required key (no values) - Type-checker config:
tsconfig.jsonwithstrict: true,noUncheckedIndexedAccess: true,exactOptionalPropertyTypes: true - Typed env:
src/env.ts(orsrc/env/{server,client}.tsif split needed); imported innext.config.tsso build fails on missing - VS Code recommendations:
.vscode/extensions.json - Onboarding doc:
CONTRIBUTING.mdwith 30-minute setup guide - Verification commit (smoke):
chore(dx): verify pre-commit hook + strict TS + env build-fail
Implementation
React + Next.js (default)
- ESLint:
next/core-web-vitals,@typescript-eslint,jsx-a11y,react-hooks,import - Prettier:
eslint-config-prettier; format on save (editor.formatOnSave: true,editor.defaultFormatter: esbenp.prettier-vscode) - Pre-commit:
husky+lint-staged;commit-msgvia@commitlint/{cli,config-conventional} - Toolchain pin:
.nvmrc,engines.node,packageManager: 'pnpm@x'(or npm/yarn) + Corepack; commit lockfile, install vianpm ci - TypeScript flags:
strict: true,noUncheckedIndexedAccess: true,exactOptionalPropertyTypes: true— runtsc --noEmitin CI, not lint-staged - Path alias:
tsconfig.json"paths": { "@/*": ["./src/*"] } - Typed env:
@t3-oss/env-nextjs+ Zod insrc/env.ts(orsrc/env/{server,client}.tswhen names are sensitive); import innext.config.tsso build fails on missing. Destructure all keys intoruntimeEnv(orexperimental__runtimeEnvfor Next.js ≥ 13.4.4) — every key must be listed. Reference viaenv.DATABASE_URL, notprocess.env.DATABASE_URL - Dev server:
next dev --turbofor Next.js 15+ - Recommended extensions:
.vscode/extensions.jsonlisting ESLint, Prettier, Tailwind CSS IntelliSense, GitLens, Error Lens
Other stacks
- Vue / Nuxt: ESLint with
eslint-plugin-vue; Prettier;@t3-oss/env-nuxtfor typed env; Nuxt dev server uses Vite by default - SvelteKit: ESLint with
eslint-plugin-svelte; Prettier withprettier-plugin-svelte; typed env via SvelteKit's$env/static/privateand$env/dynamic/public(built-in) - Angular:
angular-eslint; Prettier;@angular/clienforces strict mode; env viaenvironment.tsfiles (no Zod by default — add manually) - Other linters: Biome (Rome successor) — single binary, faster than ESLint+Prettier; Oxlint — Rust-based, fastest
- Universal: husky + lint-staged work for any git repo; path alias works in any bundler; typed env is a pattern (Zod schema + build-time validation), not a tool
Related skills
cicd-pipeline— husky + lint-staged complement CI gatingsecurity-audit— env validation hardens the secret-leak surface
Reference
- Key insight encoded: Import the env file in
next.config.tsso misconfiguration fails the build, not production runtime. Pair withtsconfigstrict+noUncheckedIndexedAccessfor full type safety — these two together catch the majority of "but it worked locally" bugs at compile time. Pin the toolchain (Node + package manager) to kill the other half of those bugs. And know the boundary: hooks are a fast local convenience (skippable, no CI), so project-widetscand the real gates belong in CI — keep lint-staged to per-file lint only.