All writing
10 min read

Designing a CI/CD Pipeline for Frontend Teams

A fast, trustworthy pipeline is infrastructure that pays for itself daily. How I structure CI stages, cache aggressively, and ship with preview deploys.

InfrastructureCI/CDDevEx

A pipeline is the one piece of infrastructure every engineer on the team touches, every day. When it is slow or flaky, it taxes the whole team's momentum and quietly erodes trust in green checkmarks. I treat the pipeline as a product with real users — my teammates — and optimize it accordingly.

Stage it for fast failure

Order jobs so the cheapest, most likely failures run first. There is no point spending four minutes on a build if a thirty-second lint would have caught the problem. I split CI into parallel jobs that fan out from a single install step, with the fast checks gating the expensive ones.

yaml
name: CI
on: pull_request

jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile

  verify:
    needs: install
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task: [lint, typecheck, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm run ${{ matrix.task }}

The matrix runs lint, typecheck, and unit tests as three independent jobs in parallel. Total wall-clock time is the slowest single task, not their sum — and a lint failure surfaces without waiting on the test suite.

Pin dependencies with a frozen lockfile

Always install with the lockfile-frozen flag in CI — pnpm install --frozen-lockfile, npm ci, or yarn --immutable. This guarantees the exact dependency tree the lockfile describes and fails loudly if the lockfile is out of sync, rather than silently resolving a different version than what runs locally. Non-reproducible installs are the root cause of an astonishing share of 'works on my machine' bugs.

Cache the right things

Two caches matter most: the package manager store and the build/test tool cache. The setup-node action handles the dependency cache when you point it at your lockfile. For build tools, cache their own incremental caches keyed on a stable hash:

yaml
- name: Cache build artifacts
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
      node_modules/.cache
    key: build-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
    restore-keys: |
      build-${{ hashFiles('pnpm-lock.yaml') }}-

The restore-keys fallback is the trick: an exact key match is ideal, but if today's commit has no cache yet, it restores the most recent cache for the same lockfile and builds incrementally on top of it. In a monorepo, a remote cache from Nx or Turborepo takes this further — unchanged packages are restored, not rebuilt, across the whole team and CI.

Preview deploys close the feedback loop

Every pull request should produce a live, shareable URL. Reviewers click instead of pulling the branch and running it locally; designers and PMs sign off on the real thing. This single practice did more for review quality on my teams than any process change.

  • Build the PR, deploy it to an isolated environment, and post the URL back as a status or comment.
  • Tear the environment down when the PR closes so preview infra does not accumulate cost.
  • Run smoke tests or Lighthouse against the preview URL to catch regressions before merge.

Make flaky tests a build-breaking bug

A pipeline is only valuable if people trust it. The moment a red build might be 'just flaky', engineers start re-running jobs reflexively and real failures slip through. Quarantine flaky tests immediately, file them as bugs, and fix or delete them. A small, reliable suite beats a large, untrustworthy one every time.

The goal is not a pipeline that is merely correct, but one the team believes. A green check should mean 'ship it' without a second thought.

Treat your pipeline like the product it is: measure its runtime, watch its failure rate, and invest in it deliberately. Minutes saved per run multiply across every engineer and every push — it is some of the highest-ROI infrastructure work you can do.