ParkEase's admin panel is a React and Vite app with nine pages: dashboard, approvals, partners, users, bookings, finance, surge, moderation and the audit log. It routes with TanStack Router, not React Router. The project file says so in capitals, because left alone, an AI will reach for React Router every time; it's what most tutorials use.
(The Android app is different: it uses Expo Router's file-based routing, the native choice for Expo. This post is about the admin.)
Here's what TanStack Router gave the admin, using the real route file.
1. Every link is type-checked
The router registers itself with TypeScript:
export const router = createRouter({ routeTree: tree }); declare module '@tanstack/react-router' { interface Register { router: typeof router; } } After that, <Link to="/finanse"> is a compile error, not a 404 an operator finds later. Rename a route and the type checker lists every link that broke. For code an AI writes quickly across many files, that's a safety net you get for one declaration.
2. Search params are validated like API input
This is the feature that sold me.
When the ledger doesn't balance, the dashboard shows a red alert listing the transactions whose debits don't equal their credits. Each is a link into the finance page's ledger explorer, opened on that exact transaction:
<Link key={t} to="/finance" search={{ txnId: t }}> {t.slice(0, 8)} </Link> On the receiving side, the finance route declares its search params with a Zod schema, the same validation library the whole project uses for API requests:
const financeSearch = z.object({ txnId: z.string().uuid().optional() }); const finance = createRoute({ getParentRoute: () => admin, path: '/finance', validateSearch: (search) => financeSearch.parse(search), component: function Finance() { return <FinancePage txnId={finance.useSearch().txnId} />; }, }); ?txnId= must be a UUID or absent. The page receives a typed txnId: string | undefined, never a raw string from the URL bar. It's a small thing, but it's the same rule as the API: everything that comes from outside the program gets validated at the boundary. A URL is outside the program.
3. An auth gate without a fake URL
Every admin page needs a signed-in admin. Instead of repeating a check in nine pages, the routes nest under one pathless layout route:
/** Every feature page sits under the admin gate, so none mounts without a session. */ const admin = createRoute({ getParentRoute: () => root, id: 'admin', component: RequireAdmin }); It has an id instead of a path, so it adds no /admin segment to any URL. Its component shows a skeleton while the session loads, redirects to the login page if there isn't one, sends a non-admin to "not authorised", and only then renders the shell with the page inside. One gate, impossible to forget on a new page, because a new page is a child of it.
4. Each page is its own download
/** Each page is its own chunk: antd tables, MapLibre and the finance screens load on first visit. */ const page = <P extends string>(path: P, component: ReturnType<typeof lazyRouteComponent>) => createRoute({ getParentRoute: () => admin, path, component }); The surge page pulls in a whole map library; the finance page has heavy tables. With lazy route components, an operator who only ever approves spaces never downloads the map. Eight of the nine pages are declared with that one helper. Finance is spelled out because it validates its search params.
Why it was good
- Mistakes move to compile time. Broken links and wrong search params fail the build, not the operator.
- URLs got the same rigour as the API. The same Zod library validates the request body and the query string.
- The structure enforces the rules. Auth is a parent route, code splitting is a helper, deep links are typed.
The cost was small: a route file that's a little more verbose than JSX routes, and having to tell the AI, once, in writing, that this project doesn't use React Router.
Next: the last post in the series, and the one I learned the most from: the AI kept saying "done" when it wasn't.
ParkEase is a peer-to-peer parking marketplace for India, built solo with Claude Code.
Code: https://github.com/Deonkar/parkease