React Admin Starter · AfifAI
Case study · Front-end platform · 2025–2026

A React admin starter that cuts new-project setup from weeks to days

Every new client back office used to begin the same way: copy the last project, strip out its business logic, and rebuild login, layout, translations and the API layer. I led the design of a reusable starter that ships all of that ready-made, so a team's first day goes into the client's domain instead of plumbing.

React 18TypeScript (strict)ViteRedux ToolkitReact Router 6i18next · RTL/LTRAxiosSCSS tokens
Client
Israeli software house (confidential)
My role
Team lead & architect (hands-on)
Timeline
Dec 2025 – Jun 2026, v2.0 on main and dev
Status
Delivered
At a glance
≈3 wk to ≈2 dfrom empty repo to a signed-in admin screen with real data (estimate)
N to 1token-refresh calls when parallel requests hit 401 at the same time
1 buildfor every environment. 7 runtime settings are read at start-up, not baked in.
7,800+lines of TypeScript/TSX each new project starts with, plus 3,400 lines of SCSS
35shared UI component families (80 components), 8 hooks, 26 utilities
2 × 2directions and themes built in: Hebrew RTL / LTR, light / dark
The problem

Every admin started as a copy of the last one

A software house delivers many back-office apps: user management, catalogs, operations dashboards. They share ≈60–70% of their front-end plumbing, but each one started by forking the previous client's code. That carried over dead features, inconsistent folder layouts and slightly different auth flows. A developer moving between projects had to relearn where the token refresh lived, how loaders worked and how Hebrew RTL was handled this time.

What I owned

One starter, one way to build

  • Set the architecture: boot order, route map, API lifecycle, layout shell and the shared component library.
  • Defined what a new project must change (title, settings, API thunks) and what it must not touch.
  • Built a reference module that shows the full list → item → sub-entity → edit/add pattern, ready to copy.
  • Ran the code reviews that shaped the v2 refactor of the header, sidebar and styles.
  • Wrote the step-by-step onboarding README that replaces tribal knowledge.
Architecture

Config first, then language, then the app

The diagram is wider than the screen: swipe sideways to see all of it →

Nothing renders until appSettings.json has loaded, so the API URL, default language and theme, company name and dev flags are always defined. The same build artifact can go to dev, staging and production; only that file changes.

Design decisions

Make the right way the easy way

Runtime config instead of build-time env vars

Settings are fetched from a static JSON file at start-up and held in one typed object. DevOps can repoint an environment without a rebuild. Trade-off: one extra request before first paint, so the app is gated behind a loader until it returns.

One API lifecycle for every call

A thunk factory plus a reducer helper give every endpoint the same idle / pending / fulfilled / rejected state, the same error shape and the global loader. Adding an endpoint is two short declarations instead of hand-writing three reducer cases. Trade-off: more Redux than a query-cache library, chosen to match the house backend's standard response envelope.

Compound components for the shell

The sidebar is 7 composable parts (Aside.Root, Nav, NavList, NavLink, NavButton, MobileHeader, Footer) and the header 3. Projects compose their own menu instead of forking the component. Trade-off: more files to learn on day one.

RTL-first, with LTR as an override

Hebrew is the default, direction comes from the language record and is set as a data-direction attribute on body. One small LTR stylesheet flips icons and offsets. Trade-off: the override uses a few forceful rules that need care when components change.

Routes as data

All routes live in one typed map with layouts, guards and redirects. A new entity is a few entries, not a new router setup. Dev flags (SKIP_AUTHENTICATION, IS_RESPONSIVE) let designers work on screens without a backend.

Engineering log

What I fixed so every project wouldn't have to

Dec 2025

No clean starting point

The previous generation had accumulated client-specific features and an older build setup.

Cut a v2 on Vite with a single neutral reference module that demonstrates parent → child entities, list, view, edit and add.

Dec 2025

Wrong tab highlighted on nested routes

On /entity/:id/edit the tab bar and its mobile dropdown disagreed about which item was active.

Each nav item carries a route pattern; the same matching logic now drives both the tab and the dropdown variant.

Dec 2025

Edit forms opened empty

Add and edit screens needed the same fields, but edit has to wait for the record.

One shared form component; in edit mode it loads the existing record and resets the form once data arrives.

Auth

A burst of 401s, a burst of refreshes

When the access token expired, every in-flight request tried to refresh it. The refreshes raced and could invalidate each other, logging the user out.

A single in-flight refresh promise is shared by all failing requests. Each original request is retried once, then the user is logged out cleanly.

Jun 2026

Header and sidebar were forked per project

Every menu change meant editing the component itself, so projects drifted apart.

Refactored both into compound components and updated all consumers in the same change.

Jun 2026

Base styles and project branding were tangled

Per-project colour and spacing tweaks were mixed into shared styles, which made upgrades painful.

Refactored the styles over two review rounds and kept one override.scss as the only place a project rebrands.

Jun 2026

An expired session stayed open

Logout only fired when another tab cleared the token. If the refresh token expired in the current tab, the user stayed on a broken screen.

One logout helper clears tokens and raises an app-level event; the route guard listens to both that event and cross-tab storage changes.

Compare

Setting up a new admin: from scratch vs with the starter

Engineering estimates in working days for one developer, based on what the starter already contains. ESTIMATE

Outcome

Where it landed

AreaBeforeWith the starter
Project kick-offFork the last client app and strip it (≈1 week)Edit title, favicon and one settings file (<1 hour)
AuthLogin, OTP and refresh rebuilt each time5 screens wired; connect the API thunks
EnvironmentsOne build per environmentOne build; 7 settings read at runtime
Token expiryN parallel refresh calls, random logouts1 shared refresh, one retry per request
Hebrew / EnglishRTL handled differently per projectDirection from the language record, one LTR override file
Code stylePer-team conventionsLint on every commit, format on staged files

Next steps I'd push for: unit tests for the API layer and guards, lazy-loaded routes, and a scaffold command that generates a new entity module from the reference.

Stack

Technology stack

Core
React 18, TypeScript 5 (strict), Vite 5
State & routing
Redux Toolkit, React Router 6, react-hook-form
Data
Axios with auth and refresh interceptors, typed thunks
i18n
i18next (HTTP backend, language detector), RTL / LTR
UI
Ant Design, PrimeReact, SCSS design tokens, light / dark
Quality
ESLint, Prettier, husky, lint-staged, shared editor extensions

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects