Skip to main content

Developer Experience & Setup

A team's velocity is bounded by its developer experience. If it takes one or two days to set up a laptop, if the dev server crashes every hour, if there is uncertainty on which env vars are required there is a tax paid on every feature, every sprint, every new hire. Good DX isn't a luxury. It's how you stop bleeding time to friction that has nothing to do with the actual problem you're solving.


Why this matters

Developer experience is where S&P's value of Evolution meets daily reality. Every minute an engineer spends fighting tooling, guessing at configuration, or waiting for a build is a minute not spent solving the client's problem. Teams with good DX onboard faster, ship with fewer "works on my machine" bugs, and retain engineers who would otherwise burn out on preventable friction.

The goal is not to automate everything or chase the latest tooling trends. The goal is to make the common path frictionless: clone, install, run, develop, commit, push. When that loop is fast and reliable, everything else (code quality, review turnaround, deployment confidence) gets easier.


The standard

Environment setup

Every S&P project should be runnable on a fresh machine with minimal manual steps. The fewer things a developer has to "just know," the healthier the project.

Node.js version management

Use nvm with an .nvmrc file at the repository root. This removes all ambiguity about which Node version the project expects.

Package manager

pnpm is the recommended package manager for new projects. It's faster than npm, uses less disk space through content-addressable storage, and handles monorepo workspaces natively. Existing projects on npm or yarn don't need to migrate for migration's sake. But new projects should start with pnpm unless there's a specific reason not to.

IDE

Cursor is the preferred IDE. It's a VS Code fork with integrated AI capabilities, so it runs all VS Code extensions and respects .vscode/ settings. Projects should commit both .vscode/ (settings, launch configs, recommended extensions) and .cursor/rules/ (AI coding standards) to the repo. For guidance on what to put in .cursor/rules/, CLAUDE.md, and how to build AI-assisted workflows, see AI-Assisted Engineering.

Engineers who prefer VS Code can use it, the configs are compatible. What matters is that the project's shared settings are committed, not which editor opens them.

For nvm configuration, pnpm setup, IDE extensions, and the complete .vscode/extensions.json, see DevOps Reference -- Environment setup and tooling.

Linting and formatting

New projects: Biome. Starting with NestJS 12 projects, use Biome as the single tool for linting and formatting. Biome replaces both ESLint and Prettier with one binary, it's faster (written in Rust), requires less configuration, and eliminates the class of bugs caused by ESLint and Prettier configs conflicting with each other.

Existing projects: ESLint + Prettier. Projects already on ESLint + Prettier don't need to migrate mid-flight. The tooling works, and the migration cost isn't justified until a natural break point (major framework upgrade, new project phase).

For Biome configuration, ESLint+Prettier setup, and the migration path between them, see DevOps Reference -- Linting and formatting.

Git hooks and commit conventions

Automate what humans will forget. Linting on save is a suggestion; linting on commit is a guarantee.

New projects: Lefthook. Use Lefthook for git hooks, it's faster than Husky, configured in a single YAML file, runs tasks in parallel, and doesn't require npm lifecycle scripts to install.

Existing projects: Husky + lint-staged. Projects already using Husky + lint-staged can stay on them.

The --max-warnings=0 flag is intentional in both setups. Warnings that are permanently ignored are worse than no warnings at all, they train the team to ignore the linter output.

For Lefthook and Husky configuration examples, see DevOps Reference -- Git hooks.

Conventional Commits

Use commitlint with @commitlint/config-conventional on the commit-msg hook (in both Lefthook and Husky setups). This enforces structured commit messages that are parseable by tooling (changelogs, release notes, CI triggers).

feat: add pagination to users endpoint
fix: handle null avatar in profile response
chore: bump biome to v2
docs: update API authentication guide
refactor: extract payment validation to shared module

Allowed types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test.

The value isn't the format itself, it's that git log --oneline becomes a readable history of what changed and why, instead of a stream of "fix stuff" and "WIP".

Monorepo structure

S&P projects typically use a pnpm workspace monorepo. The standard layout separates applications, shared libraries, and infrastructure: keeping related code in the same repo so that cross-cutting changes are atomic commits rather than coordinated multi-repo merges.

For the standard monorepo layout, pnpm workspace configuration, and shared library patterns, see DevOps Reference -- Monorepo structure.

No barrel files

Do not use barrel files (index.ts that re-exports everything from a directory). They cause several real problems:

  • Bundle size. Importing one function from a barrel pulls in the entire module graph behind it. Tree-shaking helps in production builds, but dev servers and test runners pay the full cost.
  • Circular dependencies. Barrels create implicit dependency cycles that surface as mysterious runtime errors: undefined imports, initialization order bugs. These are painful to debug because the cycle isn't visible in any single file.
  • Slow IDE performance. TypeScript's language server has to resolve every re-export in the barrel to provide autocomplete. In large projects, this measurably slows down intellisense.
  • Unclear dependency graph. With barrels, every file appears to depend on the same index.ts. Without them, imports point directly to the source file, you can trace dependencies at a glance.

Import directly from the source file:

// Do this
import { UserService } from './users/user.service';

// Not this
import { UserService } from './users';

The one exception is the public API of an FSD slice, each slice's index.ts acts as an explicit boundary defining what other layers can consume. This is intentional encapsulation, not lazy re-exporting.

Frontend architecture: Feature Sliced Design

Feature Sliced Design (FSD) is the standard frontend architecture. It organizes code into layers (app, pages, widgets, features, entities, shared) with strict import rules, each layer can only import from layers below it. This prevents the "everything in components/" problem that plagues React projects as they grow.

Why FSD: Explicit dependency direction, team scalability (features are isolated slices), and immediate structural legibility for new developers.

Supported frameworks: Next.js (default (FSD works with App Router, keep route files thin) and React + Vite (for SPAs that don't need SSR). The architecture is framework-agnostic) only the app/ layer adapts to the framework's routing mechanism.

For the full layer hierarchy, slice structure, Next.js App Router adaptation, import rules enforcement with eslint-plugin-boundaries, and React+Vite setup, see Frontend Reference. Project structure.

API type generation

The backend generates an OpenAPI specification (via @nestjs/swagger). The frontend consumes it to auto-generate TypeScript types and API client code. This eliminates a whole class of bugs where frontend and backend disagree on request/response shapes. Recommended tools: openapi-typescript for types only, orval for generated API clients with React Query hooks.

For the backend OpenAPI setup (@nestjs/swagger plugin, controller decorators, JSON spec export), see Backend Reference (API design implementation. For the frontend consumption pipeline (type generation config, TanStack Query integration), see Frontend Reference) API consumption.

Local development

Database

Projects with a database should include a docker-compose.yml that starts the required services (PostgreSQL, Redis, etc.) with a single command. No developer should need to install database software directly on their machine.

PostgreSQL 18 is the standard for new projects. Pin the major version in Docker Compose.

For the Docker Compose configuration and local database setup, see DevOps Reference -- Local development infrastructure.

Environment variables

Every app must include a .env.example file listing all required variables with placeholder values and brief comments explaining what each one does. This file is committed to the repo. The actual .env is gitignored.

# .env.example
DATABASE_URL=postgresql://dev:dev@localhost:5432/app_dev
FIREBASE_PROJECT_ID=your-project-id # Firebase console → Project settings
RESEND_API_KEY=re_xxxxxxxxxxxx # https://resend.com/api-keys
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx # https://dashboard.stripe.com/apikeys

When an engineer adds a new env var to the code, they must also add it to .env.example in the same PR. Reviewers should check for this.

Dev scripts

Use consistent script names across all apps in the monorepo:

ScriptPurpose
devStart development server with hot reload
buildProduction build
startRun production build locally
lintRun linter (Biome or ESLint)
formatRun formatter (Biome or Prettier)
checkRun linter + formatter in one pass (Biome projects)
testRun unit tests
test:e2eRun end-to-end tests
db:migrateRun database migrations
db:seedSeed database with development data

Run them via pnpm filters from the root: pnpm --filter api dev, pnpm --filter web dev.

ORM and database access

S&P doesn't mandate a single ORM, the right choice depends on the project's needs (TypeORM, Drizzle, Prisma, and Kysely are all viable). What matters more than which ORM you pick: one ORM per data layer (don't mix), migrations committed and reviewed like code, and seeds for local development.

For the full ORM comparison table, PostgreSQL conventions, migration workflow, and database security practices, see Backend Reference. Database patterns.

CI/CD

S&P projects use different CI platforms depending on the hosting context:

  • Bitbucket Pipelines: For projects hosted on Bitbucket (bitbucket-pipelines.yml)
  • CircleCI: Increasingly used across S&P projects; strong fit for complex pipelines and GCP OIDC integration (.circleci/config.yml)
  • GitHub Actions: For projects hosted on GitHub, including some client projects (.github/workflows/*.yml)

Regardless of platform, every CI pipeline should:

  1. Run lint + type-check on every PR. Fast feedback. No exceptions.
  2. Run tests on every PR. Unit and integration at minimum.
  3. Build the artifact on every PR. Catch build failures before merge, not after.
  4. Deploy automatically on merge to environment branches. Development and staging should never require manual deployment steps.
  5. Use path filtering in monorepos. Don't rebuild and redeploy the API when only the web app changed. Most CI platforms support change detection.

Project scaffolding

S&P maintains a monorepo template that serves as the starting point for new projects:

engineering-forward/templates/backend-monorepo: pnpm workspace monorepo with NestJS API, Next.js web app, shared libraries, GCP Terraform infrastructure, CI pipeline configs, and AI IDE rules.

When starting a new project:

  1. Clone the template and strip the parts you don't need (e.g., remove functions/ if no cloud functions, remove infrastructure-secure/ if standard infra is sufficient).
  2. Update package.json names, .nvmrc, and environment-specific configuration.
  3. Replace placeholder README content with project-specific setup instructions.
  4. Run through the "First day on a project" checklist below to verify everything works end to end.

The template is a starting point, not a cage. Adapt it to the project's actual needs. But start from it: reinventing monorepo setup from scratch every time is waste.

Joining an existing project

Onboarding to an existing codebase should not depend on tribal knowledge. If a new developer can't get the project running by following the README, the README is broken: fix it before moving on.

First day on a project: verification checklist:

  1. Clone the repo and run nvm use && pnpm install. Both should succeed without errors.
  2. Copy .env.example to .env and fill in any values that require access credentials (Slack the team lead if unclear which ones).
  3. Start the database with docker compose up -d.
  4. Run migrations: pnpm --filter api db:migrate.
  5. Seed development data: pnpm --filter api db:seed.
  6. Start the API dev server: pnpm --filter api dev. Hit the health endpoint.
  7. Start the web dev server: pnpm --filter web dev. Verify the page loads.
  8. Run the test suite: pnpm test. All tests should pass on a fresh clone.
  9. Make a trivial change, commit it, and verify git hooks fire (lint, commitlint).
  10. Open a draft PR to verify CI runs.

If any step fails, document the fix and update the README before doing anything else. The next person who joins will hit the same wall.


Critical thinking

When to invest in DX tooling

Not every project needs the full monorepo template with Terraform, CI/CD, and cloud functions. A short-lived prototype or a small internal tool might be fine with a simpler setup. The question to ask is: will more than two developers work on this for more than one sprint? If yes, invest in proper DX. If no, keep it minimal but don't skip the basics (git hooks, .env.example, working dev script).

When to deviate from the template

The template encodes current best practices, but it's not sacred. Deviate when:

  • The project's hosting constraints require a different CI platform or infrastructure provider.
  • A client mandates specific tooling (their ESLint config, their Git workflow).
  • The project is genuinely small enough that the monorepo structure adds more overhead than value.

When you deviate, document why in the project's README. "We use X instead of Y because [reason]" is enough.

The "works on my machine" test

Before you mark a DX setup as "done," have someone who has never touched the project follow the README on a clean machine. If they can't get to a running dev environment in under an hour, the setup isn't done, it's documented for you, not for the team.

Tooling churn

New build tools, bundlers, and dev servers appear constantly. Resist the urge to adopt something just because it's faster in a benchmark. Evaluate based on: does it solve a real friction we have today? Is it mature enough that we won't be debugging the tool instead of our code? Does the team want to maintain this migration? Stability beats novelty in production tooling.


Checklist

Use this before declaring a project's DX "ready": whether it's a new project or you've just improved an existing one.

  • .nvmrc exists and matches the Node version in CI and Dockerfiles
  • pnpm install completes without errors on a fresh clone
  • .env.example lists every required env var with comments
  • docker compose up starts all external dependencies (database, cache, etc.)
  • pnpm dev (or filtered equivalent) starts the app with hot reload
  • pnpm test passes on a fresh clone with no additional setup
  • Git hooks are configured (Lefthook or Husky): lint on pre-commit, commitlint on commit-msg
  • CI pipeline runs lint, type-check, test, and build on every PR
  • README has step-by-step setup instructions that a new developer can follow
  • .vscode/ and .cursor/rules/ are committed with shared settings
  • Frontend projects follow Feature Sliced Design layer structure
  • No barrel files (except FSD slice public API boundaries)
  • API types are auto-generated from backend OpenAPI spec
  • Monorepo uses consistent script names across all apps

AI tips

  • Use AI IDE rules to enforce project conventions. Cursor supports .cursor/rules/ files that teach the AI your project's patterns: naming conventions, module structure, testing approach. This is more effective than hoping the AI guesses your conventions from context. The S&P template includes starter rules for NestJS and Next.js. AI-Assisted Engineering covers this in depth, including CLAUDE.md structure, custom skills, and workflow orchestration.
  • Generate boilerplate with AI, then review. New NestJS modules, CRUD endpoints, FSD feature slices, these follow predictable patterns. Let AI generate the scaffold, then review it against the project's actual conventions. Don't let AI output bypass your git hooks.
  • Automate env var documentation. When you add a new dependency that requires configuration, ask AI to update .env.example with the new variable, a placeholder value, and a comment explaining where to get the real value.
  • Troubleshoot setup issues faster. When onboarding hits an error, paste the full error output into AI. Setup errors are usually environment-specific and well-documented. AI is good at matching symptoms to solutions.
  • Generate Docker Compose services. Describe what your app needs (PostgreSQL 18 with PostGIS, Redis, local S3-compatible storage) and let AI generate the docker-compose.yml. Review ports, volumes, and version pins.
  • Migrate ESLint configs to Biome. Paste your existing .eslintrc and .prettierrc and ask AI to generate the equivalent biome.json. Biome's rule names differ from ESLint's, so a manual pass is still needed, but AI gets you 90% there.
  • Generate OpenAPI client types. If you're setting up the API type generation pipeline for the first time, describe your backend's OpenAPI setup and let AI generate the orval or openapi-typescript config.

Resources

S&P internal:

Tooling:

Architecture:

NestJS:

Frontend: