Why the App Router matters for fintech
Financial dashboards have specific requirements that make Next.js 16's App Router particularly well-suited — and occasionally frustrating. The data is sensitive, so server-side rendering with proper auth checks is non-negotiable. The data is also frequently real-time or near-real-time, so the boundary between server and client components needs careful management. And the UI is often deeply nested — admin panels, user dashboards, and transaction detail modals all stacked — which is exactly the use case the App Router's layout system was designed for.
Server components as the auth boundary
The most important architectural decision in a fintech App Router project is treating server components as your auth boundary. Every page or layout that renders sensitive data should call your auth function — `auth()` from NextAuth v5, for example — at the top of the component and redirect to login if the session is absent or insufficient.
Do not rely solely on middleware for this. Middleware runs at the edge and protects routes, but it cannot access your database or verify role-based permissions. A server component that explicitly checks `session.user.role === 'ADMIN'` before fetching admin data is a more robust guarantee than a middleware regex pattern alone.
The practical pattern: create a `requireAuth()` utility that calls `auth()`, throws (or redirects) if there's no valid session, and returns the typed user object. Call it at the top of every protected server component. This makes the auth dependency explicit and auditable.
Data fetching: the server/client split
In financial dashboards, the distinction between server components (data fetching, sensitive logic) and client components (interactivity, real-time updates) maps naturally onto the product architecture. Account balances and transaction history can be server-fetched on load — they don't change fast enough to require WebSocket streaming. Real-time exchange rates, live transaction feeds, and latency monitors, on the other hand, should use polling or SSE from client components.
A common pitfall is fetching too much on the server and passing huge props trees to client components, or fetching too little and making waterfalled API calls from the client. The right approach is to do the initial heavy lift on the server — fetch the account summary, the recent transactions, the user profile — and hand that data to client components as props. Client components then supplement with incremental updates.
Avoid `use client` at the layout level if you can. A layout marked `use client` propagates that constraint to all its children, preventing them from being server components. Keep layouts as server components and push `use client` as far down the tree as the first interactive element requires.
Route groups for multi-role UIs
Fintech platforms typically have at least three distinct user experiences: the public marketing site, the authenticated user dashboard, and the admin back-office. Route groups — the `(folder)` syntax in the App Router — are the clean way to separate these without polluting the URL.
Structure your app as `(public)`, `(dashboard)`, and `(admin)` route groups, each with its own layout. The public layout includes the marketing navbar and footer. The dashboard layout includes the user sidebar and session checking. The admin layout includes the dark-mode admin sidebar and stricter role checking. This separation means you never accidentally render an admin component in a public context.
Framer Motion and the client boundary
Framer Motion is widely used in fintech product UIs for smooth transitions and animated data visualisations. In the App Router, there's one critical rule: any component that uses `motion.*` or `AnimatePresence` must be a client component. This is easy to forget when you're animating a component that otherwise has no client-side logic.
A useful pattern is to create thin wrapper components — `FadeIn.tsx`, `SlideUp.tsx` — that are marked `use client` and simply wrap their children in a `motion.div` with a preset animation. Your page-level server components then import these wrappers and pass server-rendered content as children. This keeps the `use client` boundary minimal while still getting the animations you want.
One more note: the `ease` property in Framer Motion's transition config must be a `[number, number, number, number]` array when used in TypeScript strict mode — not a string like `'easeInOut'`. This catches many teams by surprise when deploying to production.
