AboutExpertiseProjectsJourneyBlogContact
Discuss
AboutExpertiseProjectsJourneyBlogContactDiscuss

Yao David Logan

Software Engineer fullstack specialized in SaaS, business automation and scalable web/mobile platforms.

NavigationExpertiseProjectsJourneyBlogContact
LinksGitHubLinkedInEmail
© 2026 Yao David Logan. All rights reserved.
Next.js 14 App Router in production — lessons after 6 months on Nexus
Back to blog

Retour d'experience

Xin@

Next.js 14 App Router in production — lessons after 6 months on Nexus

YDLYao David Logan16 min readSeptember 18, 2026

CategoryRetour d'experience

Read16 min read

PublishedSeptember 18, 2026

Views0

Shares0

Comments0

Contents
  1. 011. The context
  2. 022. What worked immediately
  3. 033. What hurt
  4. 044. Server Actions — the real revolution
  5. 055. The Server Components cost
  6. 066. Vercel vs self-hosted deployment
  7. 077. SEO and pure SSR
  8. 088. Difficult migrations
  9. 099. Pitfalls to avoid
  10. 1010. What I'd do again
  11. 1111. What I wouldn't redo
  12. 1212. Closing

Server Components, streaming, nested layouts, Server Actions, parallel routes: what actually works and what stings in production on a fintech platform.

Six months of production on Next.js 14 with App Router and Server Components. What worked, what hurt, what I'd do again and what I wouldn't.

1. The context

On Nexus (fintech platform), I migrated a Pages Router stack from Next.js 13 to App Router on 14. Promises: Server Components, streaming, nested layouts, simplified data fetching, parallel routes. Six months later, the verdict is nuanced. Massive value, but real pitfalls that cost in production.

This article condenses the learnings. No generic "Next.js is great": what actually works, what derails, and how to recover.

2. What worked immediately

2.1 Nested layouts

Huge advantage. Before, we had a global _app.tsx and shared Layout components glued by hand. Now each segment has its own layout.tsx that doesn't re-render when navigating among its children.

TSX
1// app/dashboard/layout.tsx
2export default function DashboardLayout({ children }: { children: React.ReactNode }) {
3 return (
4 <div className="dashboard">
5 <DashboardSidebar />
6 <main>{children}</main>
7 </div>
8 );
9}

The sidebar stays static between /dashboard/portfolio and /dashboard/transactions. Big perceived UX gain.

2.2 Streaming + Suspense

Being able to stream a page section by section changes the loading feel. The hero appears immediately, data blocks come progressively. On Nexus, LCP dropped from 3.2 s to 1.4 s average just from this.

TSX
1// app/dashboard/page.tsx
2export default async function DashboardPage() {
3 return (
4 <>
5 <PortfolioHeader />
6 <Suspense fallback={<TransactionsSkeleton />}>
7 <RecentTransactions />
8 </Suspense>
9 <Suspense fallback={<ChartSkeleton />}>
10 <MarketChart />
11 </Suspense>
12 </>
13 );
14}

2.3 Per-segment generateMetadata

Per-page metadata became trivial. No more next/head orchestration.

TSX
1export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
2 const project = await fetchProject(params.slug);
3 return {
4 title: `${project.title} — Portfolio`,
5 description: project.excerpt,
6 openGraph: { images: [project.image] },
7 };
8}

3. What hurt

3.1 The Server/Client boundary is subtle

"use client" at the top of a file marks the entire tree as client. A lib depending on window imported transitively breaks silently. The first month I had several builds passing in dev and crashing in prod.

Discipline: isolate client components in their own files, keep pages and layouts as server by default.

TSX
1// app/dashboard/page.tsx — Server Component
2import { InteractiveChart } from "@/components/InteractiveChart";
3 
4export default async function DashboardPage() {
5 const data = await fetchData(); // server
6 return <InteractiveChart data={data} />; // client component, separate file
7}
8 
9// components/InteractiveChart.tsx
10"use client";
11import { useState } from "react";

3.2 Aggressive fetch cache

By default, fetch in a Server Component is "force-cache". Nobody expects it first time. On Nexus, the dashboard displayed a frozen balance for 60 minutes.

TSX
1// ❌ Bad — balance frozen at first request
2const balance = await fetch("/api/balance").then(r => r.json());
3 
4// ✅ Good — refetch each request
5const balance = await fetch("/api/balance", { cache: "no-store" }).then(r => r.json());
6 
7// ✅ Also good — regular revalidate
8const balance = await fetch("/api/balance", { next: { revalidate: 30 } }).then(r => r.json());

Rule of thumb: for any user or financial data, cache: "no-store". For marketing/blog content: revalidate per acceptable freshness.

3.3 Hydration and flash

When a Server Component returns content different from what the client recomputes (typically because of Date.now() or localStorage), you get a hydration mismatch. Console error + visual flash.

Working patterns:

  • For conditional rendering based on window or localStorage → useEffect after mount, never in initial render
  • For "X minutes ago" dates → compute server-side only, send an ISO to the client which formats in its timezone

3.4 Parallel routes — powerful but esoteric

Parallel routes (@modal, @notifications) are an expressiveness bomb. But docs are thin and patterns sparsely documented. Took me time to figure out how to intercept a route to show a modal over a page without losing the URL.

Plain Text
1app/
2 layout.tsx
3 page.tsx ← home
4 @modal/
5 default.tsx ← nothing by default
6 (.)projects/[slug]/page.tsx ← intercepted: opens a modal
7 projects/
8 [slug]/page.tsx ← direct navigation: full page

Clicking a project card from home opens a modal (intercepted route). Sharing the URL opens the full page. Same router serves both. Huge UX gain once mastered.

4. Server Actions — the real revolution

Being able to write a server-side mutation without building a REST API is what saved me the most time.

TSX
1// app/(auth)/login/actions.ts
2"use server";
3import { redirect } from "next/navigation";
4 
5export async function loginAction(formData: FormData) {
6 const email = formData.get("email") as string;
7 const password = formData.get("password") as string;
8 
9 const session = await authService.login(email, password);
10 if (!session) return { error: "Invalid credentials" };
11 
12 cookies().set("session", session.token, { httpOnly: true, secure: true });
13 redirect("/dashboard");
14}

Pros:

  • No REST API to wire
  • Server-side validation, no client bypass risk
  • Native progressive enhancement (works without JS)

Limits:

  • No native support for progress streams during mutation
  • Network errors surfaced as generic messages on client — need to wrap for better UX

5. The Server Components cost

Marketing talks of "minimal JavaScript bundle". True and false.

True: a pure Server Component ships zero JS. False: as soon as you have client components (forms, interactions), the bundle grows. On Nexus, after 4 months, first-party JS bundle was at 280 KB gzipped — more than we had with Pages Router and some manual optimization.

Lesson: Server Components reduce JS by default, but don't exempt you from watching the bundle. next-bundle-analyzer stays essential.

6. Vercel vs self-hosted deployment

Next.js 14 on Vercel: everything works. ISR, Edge Functions, streaming, distributed cache.

Next.js 14 self-hosted: half the features need specific setup. For Nexus running on dedicated infrastructure (fintech data sovereignty):

  • ISR with revalidate: OK with a shared filesystem cache between instances
  • Streaming: OK if Nginx doesn't buffer (X-Accel-Buffering: no)
  • Edge Functions: not available, Node Runtime fallback
  • Image Optimization: needs an external CDN or local proxy

The self-hosted cost is underestimated in official docs. Plan 1–2 weeks of setup and tuning if you skip Vercel.

7. SEO and pure SSR

For a blog or content site, Server Components + ISR is unbeatable. For Nexus blog (subdomain, ~200 articles), migrating from Gatsby halved LCP and improved Core Web Vitals.

Patterns:

  • generateStaticParams to pre-generate slugs at build
  • revalidate: 3600 to refresh hourly
  • Dynamic sitemap via app/sitemap.ts
  • RSS via app/feed.xml/route.ts

8. Difficult migrations

8.1 getServerSideProps → Server Component

Mechanical but often tricky. Signature changes, redirect pattern too.

TSX
1// Before
2export async function getServerSideProps(ctx) {
3 const session = await getSession(ctx);
4 if (!session) return { redirect: { destination: "/login", permanent: false } };
5 return { props: { user: session.user } };
6}
7 
8// After
9export default async function Page() {
10 const session = await getSession();
11 if (!session) redirect("/login");
12 return <Dashboard user={session.user} />;
13}

8.2 next/router → next/navigation

Hooks change: useRouter, usePathname, useSearchParams instead of a single useRouter. Mechanical refactor.

8.3 API Routes → Route Handlers

pages/api/foo.ts becomes app/api/foo/route.ts with exported GET, POST functions. Signature changes, typing becomes stricter.

9. Pitfalls to avoid

PitfallSymptomFix
"use client" too broadJS bundle bloatIsolate client components in dedicated files
Implicit fetch cacheFrozen datacache: "no-store" or revalidate
Hydration mismatchConsole warnings + flashRuntime computations in useEffect
ISR on user dataOne user's data shown to anotherNever ISR on personalized content
Self-hosted without tuningStreaming buffer, broken ISRNginx config + shared filesystem cache
Server Actions without validationClient bypass riskAlways Zod or server validator
Undocumented parallel routesTeam lostREADME + explicit diagrams

10. What I'd do again

  • Server Components for content pages
  • Systematic Streaming + Suspense
  • Server Actions for simple mutations
  • Nested layouts to structure menu/sidebar
  • ISR for marketing and blog
  • generateMetadata for per-page SEO

11. What I wouldn't redo

  • Migrate a highly interactive app just "to be current" — gain is small, cost is high
  • Rely on default fetch cache — always explicit
  • Self-host on exotic infrastructure — go Vercel or very standardized Kubernetes

12. Closing

App Router is a real advance for content apps and well-structured portals. For highly interactive apps, gain is moderate and pitfalls real.

Starting today: go App Router. Big Pages Router base: migrate section by section, never big bang.

The 4–8 weeks investment in Server Components patterns pays back monthly. But you must accept the learning curve.

Comments

Reader reactions

No comment yet

No spam — email is only used to verify your identity.

·

Be the first to share your reaction.

←PreviousAutonomous LLM agents on the phone — Vapi + SIP + GPT-4 field notesTech · 17 min readNextLawful scraping at scale — Prospect-Pro AI field notesRetour d'experience · 16 min read→
Next conversation

Turn this reading into a product decision.

If this topic feels close to a real product problem, I can help on diagnosis, architecture, backend, interface and automations that make a platform usable in production.

Format
Full-time, freelance, long mission
Focus
SaaS, API, back-office, automation
Discuss the topicDownload CV

Newsletter

Get the next technical notes.

A short selection on SaaS, backend architecture, business automation and product quality. No noise, only applicable ideas.