# Developer Experience

> Set up DX baseline — ESLint + Prettier + husky + lint-staged + commitlint, strict TypeScript, path alias, t3-env for typed env vars, Node/package-manager pinning, .editorconfig, .vscode/extensions.json, CONTRIBUTING.md. Use at project start, during team onboarding, when ESLint warnings pile up, or local builds slow down. Coordinates with cicd-pipeline (local pre-commit gating complements CI) and security-audit (typed env validation hardens the secret-leak surface).

- Skill: `jaykim88/developer-experience` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/developer-experience`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/developer-experience/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/developer-experience

---


# 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

1. **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 `.editorconfig` for cross-editor consistency (indent, line endings) — not everyone uses the same editor
   - (ESLint / Prettier — see Implementation)

2. **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-msg` hook (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-verify` and don't run in CI. CI must re-run lint / type-check / test as the real gate (see `cicd-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 (`packageManager` field + Corepack) so everyone runs identical versions — most "works on my machine" bugs are a version mismatch
   - Commit the lockfile; use `npm ci` (not `npm install`) in CI and onboarding for reproducible installs; never mix package managers (one lockfile)

3. **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 be `undefined`) and `undefined`-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 --noEmit` in 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). Run `tsc` in CI (or a `pre-push` hook); keep per-file lint in pre-commit
   - (TS `strict` / `noUncheckedIndexedAccess` / `exactOptionalPropertyTypes` — see Implementation)

4. **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)

5. **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 is `string | 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.example` listing every required key (no values) so onboarding and the typed schema stay in sync
   - (t3-env — see Implementation)

6. **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)

7. **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)

8. **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 commit` and auto-lints (staged files only)
- [ ] `commit-msg` hook enforces Conventional Commits
- [ ] Node + package manager pinned (`.nvmrc` / `engines` / `packageManager`); lockfile committed
- [ ] TypeScript strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes enabled
- [ ] Project-wide `tsc` runs 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.example` lists required keys
- [ ] New team member can clone → dev server running in ≤ 30 min following CONTRIBUTING.md
- [ ] `.editorconfig` + `.vscode/extensions.json` present

## Output
- **Linter + formatter config**: `.eslintrc.{json,cjs}` + `.prettierrc` + `eslint-config-prettier` + `.editorconfig`
- **Pre-commit hook**: `.husky/pre-commit` + `lint-staged` config in `package.json`; `.husky/commit-msg` + `commitlint.config.{js,ts}`
- **Toolchain pin**: `.nvmrc` + `engines.node` + `packageManager` field; committed lockfile
- **Env example**: `.env.example` listing every required key (no values)
- **Type-checker config**: `tsconfig.json` with `strict: true`, `noUncheckedIndexedAccess: true`, `exactOptionalPropertyTypes: true`
- **Typed env**: `src/env.ts` (or `src/env/{server,client}.ts` if split needed); imported in `next.config.ts` so build fails on missing
- **VS Code recommendations**: `.vscode/extensions.json`
- **Onboarding doc**: `CONTRIBUTING.md` with 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-msg` via `@commitlint/{cli,config-conventional}`
- Toolchain pin: `.nvmrc`, `engines.node`, `packageManager: 'pnpm@x'` (or npm/yarn) + Corepack; commit lockfile, install via `npm ci`
- TypeScript flags: `strict: true`, `noUncheckedIndexedAccess: true`, `exactOptionalPropertyTypes: true` — run `tsc --noEmit` in CI, not lint-staged
- Path alias: `tsconfig.json` `"paths": { "@/*": ["./src/*"] }`
- Typed env: `@t3-oss/env-nextjs` + Zod in `src/env.ts` (or `src/env/{server,client}.ts` when names are sensitive); import in `next.config.ts` so build fails on missing. Destructure all keys into `runtimeEnv` (or `experimental__runtimeEnv` for Next.js ≥ 13.4.4) — every key must be listed. Reference via `env.DATABASE_URL`, not `process.env.DATABASE_URL`
- Dev server: `next dev --turbo` for Next.js 15+
- Recommended extensions: `.vscode/extensions.json` listing ESLint, Prettier, Tailwind CSS IntelliSense, GitLens, Error Lens

### Other stacks
- **Vue / Nuxt**: ESLint with `eslint-plugin-vue`; Prettier; `@t3-oss/env-nuxt` for typed env; Nuxt dev server uses Vite by default
- **SvelteKit**: ESLint with `eslint-plugin-svelte`; Prettier with `prettier-plugin-svelte`; typed env via SvelteKit's `$env/static/private` and `$env/dynamic/public` (built-in)
- **Angular**: `angular-eslint`; Prettier; `@angular/cli` enforces strict mode; env via `environment.ts` files (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 gating
- `security-audit` — env validation hardens the secret-leak surface

## Reference
- **Key insight encoded**: Import the env file in `next.config.ts` so misconfiguration fails the build, not production runtime. Pair with `tsconfig` `strict` + `noUncheckedIndexedAccess` for 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-wide `tsc` and the real gates belong in CI — keep lint-staged to per-file lint only.

