Next.js Application Development¶
The bar: you can build a full-stack feature in Next.js — routing, rendering, data fetching, mutations, and styling — and ship it to a live platform that real veterans use.
Next.js is the framework that turns React from a UI library into a production application. It gives you routing, server-side rendering, data fetching, an API layer, and a build pipeline in one box, so you spend your time on the feature instead of wiring plumbing. This is the exact stack behind the Vets Who Code platform — the same codebase you will be contributing to.
This module teaches the modern App Router mental model end to end. Learn it faithfully: it is where the framework is going, and the concepts — server-first rendering, colocated data fetching, mutations close to the UI — carry across the whole ecosystem. Where a concept maps to something specific in the VWC repo, this page tells you the honest translation, because the production app runs the Pages Router today.
10.1 App Router Architecture¶
Objective: understand the fundamentals of the Next.js 14+ App Router.
The App Router builds routes from the filesystem. A folder inside app/ becomes a URL segment, and a page.tsx inside it becomes the rendered page. You get nested layout.tsx files that wrap child routes and persist across navigation, plus template.tsx when you need a fresh instance on every navigation instead. Special files loading.tsx and error.tsx give you built-in loading skeletons and error boundaries without hand-rolling either.
Beyond the basics, folders in parentheses become route groups that organize code without adding a URL segment, [id] is a dynamic route, and [...slug] is a catch-all. Parallel and intercepting routes let you render multiple slots or overlay a route (like a modal) without leaving the current page.
The gotcha for self-taught engineers coming from Pages Router: file names are load-bearing. A component named page.tsx in the wrong folder, or a layout.tsx that forgets to render {children}, fails silently or blanks the page. Learn the reserved file names cold.
Drill:
- Build a nested route tree with
layout.tsx,page.tsx,loading.tsx, anderror.tsx. - Add a dynamic route
[id]and a catch-all[...slug]. - Group routes with
(marketing)and(app)folders and confirm the URLs stay clean. - Wire an intercepting route to open content as a modal.
10.2 Server and Client Components¶
Objective: choose the right rendering strategy for each component.
In the App Router, components are Server Components by default. They run on the server, can touch the database or filesystem directly, ship zero JavaScript to the browser, and keep your bundle small. The moment you need interactivity — state, effects, event handlers, browser APIs — you opt into a Client Component by putting the 'use client' directive at the top of the file.
The skill is composition. Keep pages server-first and push 'use client' down to the smallest leaf that actually needs it. You can pass Server Components as children into Client Components, but anything crossing that boundary must be serializable — no functions, no class instances, no Dates that survive as Dates. That serialization boundary is where most confusing bugs live.
The common mistake: slapping 'use client' at the top of a page "to make errors go away." That drags the whole subtree to the client, kills the performance win, and hides the real problem.
Drill:
- Convert an interactive component to a Client Component with
'use client'and confirm the rest stays on the server. - Pass a Server Component as
childreninto a Client Component. - Trigger a serialization error on purpose, then fix it by moving the boundary.
10.3 Data Fetching¶
Objective: fetch and manage data across server and client in Next.js.
Server Components let you fetch data right where you render it — await a query or a fetch() call inside the component, no useEffect, no loading flag. Next.js extends fetch with caching and revalidation: results are cached by default, and you control freshness with revalidate options. This is what decides static vs dynamic rendering — a fully cacheable route is prerendered, while one that reads dynamic data renders per request.
For content that changes occasionally, Incremental Static Regeneration (ISR) rebuilds a static page in the background on an interval so users get static speed with fresh-enough data. For genuinely client-side, user-specific data, reach for SWR — stale-while-revalidate hooks that handle caching and refetching. And Suspense lets you stream a page in chunks, showing the shell instantly while slower data fills in.
The gotcha: caching is aggressive by default. If your data looks stale, you probably need explicit revalidate or a dynamic opt-out, not a page refresh.
In the VWC repo, which runs the Pages Router, server data fetching maps to getServerSideProps (per-request) and getStaticProps with revalidate (static + ISR). Same ideas, different entry points.
Drill:
awaita data fetch directly inside a Server Component.- Set
revalidateon afetchand observe cached vs fresh responses. - Build an ISR page and confirm it regenerates on schedule.
- Add client-side
SWRfor user-specific data and wrap a slow section inSuspense.
10.4 Server Actions¶
Objective: handle form submissions and mutations without a separate API call.
Server Actions are functions marked with the 'use server' directive that run on the server but are called straight from your components — no fetch, no hand-written endpoint. Wire one to a <form action={...}> and the submission runs server-side, which means forms work even before JavaScript loads. That is progressive enhancement you get for free.
Do the real work: validate input and return typed errors the UI can render, apply optimistic updates with useOptimistic so the interface responds instantly, and call revalidatePath or revalidateTag after a mutation so the displayed data reflects the change.
The mistake to avoid: trusting client input. A Server Action is a public endpoint. Validate every field and check authorization inside the action, not just in the form.
Drill:
- Write a
'use server'action and bind it to a<form action>. - Add server-side validation that returns field errors.
- Layer in
useOptimisticfor an instant UI response. - Call
revalidatePathafter the mutation and confirm the list updates.
10.5 API Routes¶
Objective: build API endpoints inside a Next.js app.
When you need a real HTTP endpoint — a webhook, a third-party callback, a public JSON API — the App Router uses route handlers: a route.ts file exporting functions named for HTTP verbs (GET, POST, PUT, DELETE). Each receives a Request and returns a Response, so you work with the standard web platform objects.
Production endpoints need more than a happy path. Use middleware.ts to authenticate requests before they hit the handler, add rate limiting to protect against abuse, and configure CORS headers when a browser on another origin needs access.
The VWC production app runs the Pages Router, so its endpoints live in src/pages/api/ as handlers that take (req, res). The concepts transfer directly. In that codebase, wrap handlers with requireAuth / requireRole from @/lib/rbac rather than rolling your own auth check, and use the shared Prisma singleton — never new PrismaClient() per file.
Drill:
- Build a route handler exporting
GETandPOST. - Parse a JSON body and return the right status codes.
- Add auth in middleware and a simple rate limit.
- Set CORS headers and verify a cross-origin request.
10.6 Styling in Next.js¶
Objective: style applications with modern CSS approaches.
Tailwind CSS is the default here: utility classes in your markup, no context-switching to a stylesheet. Pair it with CSS Modules (*.module.css) for the rare component that needs scoped, hand-written CSS, and a global stylesheet plus CSS variables for design tokens like brand colors and spacing.
Build responsive layouts with Tailwind's breakpoint prefixes, implement dark mode with its dark: variant, and handle motion with Tailwind's transition and animation utilities, dropping to raw CSS keyframes when you need something custom.
One VWC-specific trap worth burning into memory: the Tailwind config uses the tw- prefix. A class written as flex or p-4 does nothing — it must be tw-flex, tw-p-4. Unprefixed utility classes fail silently, which is maddening to debug if you do not know the rule.
Drill:
- Style a component with Tailwind, then extract one piece into a CSS Module.
- Define design tokens as CSS variables and consume them.
- Add responsive breakpoints and a working
dark:mode toggle. - Animate a transition with Tailwind utilities.
10.7 Performance Optimization¶
Objective: optimize a Next.js app for production.
Next.js ships optimization tooling you should actually use. next/image handles resizing, lazy loading, and modern formats, so images stop tanking your page. next/font self-hosts fonts and removes layout shift from font swapping. Code splitting and dynamic() imports keep heavy components out of the initial bundle until they are needed.
Measure before you optimize. Run bundle analysis to find what is bloating the build, and track Core Web Vitals (LCP, CLS, INP) as your scoreboard rather than guessing. Layer in caching strategies — the fetch cache, ISR, and static rendering from earlier sections — to cut server work.
The self-taught mistake: optimizing by feel. Swapping libraries or memoizing everything without a profiler wastes hours. Get a measurement first, fix the biggest number, measure again.
Drill:
- Replace
<img>tags withnext/imageand confirm lazy loading. - Load fonts through
next/fontand check for zero layout shift. - Lazy-load a heavy component with
dynamic(). - Run a bundle analyzer and record your Core Web Vitals before and after.
10.8 Hands-On Lab: Full-Stack Feature¶
Objective: build a complete feature with data fetching, mutations, and UI.
Build one real CRUD feature end to end — the whole loop from database to deployed URL.
What you ship
- A CRUD feature driven by Server Actions for create, update, and delete.
- Optimistic updates so the UI responds instantly before the server confirms.
- Proper loading and error states using
loading.tsx/error.tsx(orSuspense) so the feature never shows a blank or broken screen. - The finished feature deployed to Vercel on a live URL you can hand to someone.
On the VWC codebase
The production Vets Who Code app currently runs the Pages Router, not the App Router this module teaches. Translate as you go: App Router route handlers become src/pages/api/ handlers, and server-side data fetching (Server Components) becomes getServerSideProps / getStaticProps. Remember the house rules when you contribute — the tw- prefix on every Tailwind class, the shared Prisma singleton, Biome (not ESLint/Prettier) for lint and format, and npm only.