← Back to blog

May 09, 2026 • 3 min read

Scaffolding a Modern Stack

What it looks like to start a real project with React 19, TanStack Router, Tailwind CSS v4, shadcn/ui, and GitHub Actions from day one.

TL;DR

Frontend scaffolding is infrastructure. Invest in tooling (ESLint, husky, CI) before writing a single feature component.

  • react
  • tanstack-router
  • tailwind
  • typescript
  • tooling

Green Algeria Map started with a pnpm create vite and a question: how do you set up a project so it stays maintainable past week one?

Here’s what went into the initial scaffold.

The Stack

  • React 19 — Concurrent features, improved server components (not used here), use() hook
  • TanStack Router — Type-safe routing with loaders, search params, and file-based route generation
  • Tailwind CSS v4 — CSS-first config via @theme, OKLCH colors, no tailwind.config.ts
  • shadcn/ui — Copy-paste primitives (button, dialog, input, toast) styled with Tailwind
  • Leaflet + react-leaflet — Map rendering with 10 demo zones
  • ESLint + Prettier — Format-on-save, consistent imports via eslint-plugin-import
  • husky + lint-staged — Pre-commit hooks that run check, lint, format
  • GitHub Actions — CI from commit one, split by path (frontend vs backend)

TanStack Router

The killer feature: type-safe routes. No more string matching route names.

const router = createRouter({
  routeTree,
  defaultPreload: "intent",
  context: { queryClient },
});

Route files are co-located with features:

src/routes/
├── __root.tsx
├── index.tsx
├── dashboard.tsx
├── zones/
│   ├── index.tsx
│   └── $zoneId.tsx
└── auth/
    ├── signin.tsx
    └── signup.tsx

Loaders run before the component renders and can return typed data:

export const Route = createFileRoute("/zones/")({
  loader: async ({ context }) => {
    const zones = await context.queryClient.fetchQuery({
      queryKey: ["zones"],
      queryFn: () => api.getZones(),
    });
    return { zones };
  },
  component: ZonesPage,
});

Tailwind v4: CSS-first Config

No more tailwind.config.ts. Colors and breakpoints go in your CSS file:

@import "tailwindcss";

@theme {
  --color-primary: oklch(0.55 0.18 160);
  --color-primary-hover: oklch(0.5 0.18 160);
  --color-surface: oklch(0.97 0 0);
  --color-surface-dark: oklch(0.18 0 0);
}

OKLCH colors are perceptually uniform — they look right in both light and dark mode without manual tuning.

shadcn/ui

It’s not a package you install. You run npx shadcn@latest add button and it copies the source into src/shared/components/ui/. You own every pixel. No version pinning, no breaking updates.

CI from Day One

Every push runs the quality gate:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm install
      - run: pnpm check
      - run: pnpm lint
      - run: pnpm build

Path filters keep it fast — backend changes only trigger backend jobs, frontend changes trigger frontend jobs.

What This Unlocked

With this scaffold in place, adding features became mechanical:

  • New route → write route file → done
  • New API call → write TanStack Query hook → done
  • New UI pattern → npx shadcn add → done

No build config drift. No lint rule disagreements. No “works on my machine”.

Lesson

The most productive decision was spending the first 2 days on tooling, not features. Every hour spent on CI, lint, and type safety saved about 5 hours of debugging later.