| name | developer-experience |
| description | 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). |
| license | MIT |
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
.editorconfig for 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-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)
-
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)
-
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)
-
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)
-
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
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.