When you decide to Hire Next.js Developers for your product, you quickly realize that not all candidates are equal. The framework has evolved massively since the Pages Router days, and many developers still live in that old world. This guide cuts through the noise and gives you a precise, technical hiring framework to identify engineers who genuinely understand the App Router, React Server Components (RSC), and Edge Functions, so you stop paying for developers who Google everything on the job.
Why the Knowledge Gap Is Wider Than You Think
Next.js 13 and beyond introduced a mental model shift, not just a syntax change. The App Router is not simply a new file convention. It is a full rethinking of how data fetching, rendering, caching, and routing coexist in a React application.
Developers who learned Next.js before 2023 are often fluent in:
-
getServerSideProps/getStaticProps - Pages under
/pagesdirectory - API routes via
/pages/api - Client-side data fetching with SWR or React Query
But modern Next.js (14 and 15) expects you to think in:
- Server Components vs Client Components
- Layouts, Templates, and nested routing via
/app - Route Handlers instead of API routes
- Streaming with Suspense
- Edge and Node.js runtimes
Understanding this gap helps you write better job descriptions and ask the right interview questions.
Section 1: App Router Deep Dive
What to Look For
A strong candidate should be able to explain the /app directory structure without hesitation and articulate why nested layouts exist.
Interview Question to Ask
"Walk me through how you would set up a dashboard with a shared sidebar that does not re-render on route changes, while each dashboard page fetches its own data server-side."
Expected Answer Pattern
They should mention:
- A root
layout.tsxthat wraps all pages - A nested
dashboard/layout.tsxthat holds the sidebar as a Server Component - Individual
page.tsxfiles for each dashboard route that fetch their own data usingasync/awaitdirectly in the component
Code They Should Be Comfortable Writing
// app/dashboard/layout.tsx
import Sidebar from "@/components/Sidebar";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
);
}
// app/dashboard/analytics/page.tsx
async function getAnalyticsData() {
const res = await fetch("https://api.example.com/analytics", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
return (
<section>
<h1>Analytics</h1>
<p>Total Users: {data.totalUsers}</p>
</section>
);
}
A weak candidate will suggest using useEffect for data fetching here or ask where to put getServerSideProps. That is your signal.
Section 2: React Server Components (RSC)
Why This Is a Frequent Lie on Resumes
Server Components are architecturally different from anything React developers were used to before. Many candidates claim to know them but cannot explain the boundary rules.
The Boundary Rule Test
Ask this directly: "What happens if you import a Client Component inside a Server Component and vice versa?"
The correct answer:
- A Server Component can import and render a Client Component.
- A Client Component cannot import a Server Component. However, a Server Component can be passed as a
childrenprop to a Client Component.
Code to Illustrate During the Interview
// components/ProductCard.tsx <-- Server Component (default)
import AddToCartButton from "./AddToCartButton"; // Client Component
export default async function ProductCard({ id }: { id: string }) {
const product = await fetchProduct(id); // Direct DB or API call
return (
<div>
<h2>{product.name}</h2>
<p>{product.price}</p>
<AddToCartButton productId={id} />
</div>
);
}
// components/AddToCartButton.tsx <-- Client Component
"use client";
import { useState } from "react";
export default function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Added" : "Add to Cart"}
</button>
);
}
Ask them: "Why does AddToCartButton need use client?" They should say: because it uses useState, which is a browser-side hook that has no meaning on the server.
Bonus Question for Senior Candidates
"How do Server Components affect bundle size?"
Expected answer: Server Components are never sent to the client as JavaScript. The component runs on the server, returns serialized UI, and only the HTML output reaches the browser. This can dramatically reduce client-side JavaScript.
Section 3: Edge Functions and the Runtime Model
What Edge Functions Actually Are
Edge Functions in Next.js run on a distributed runtime (like Cloudflare Workers or Vercel Edge Network) rather than a centralized Node.js server. They are closer to the user geographically and start up nearly instantly because they do not boot a full Node.js process.
The Tradeoff Most Developers Miss
Edge runtime does not support all Node.js APIs. Specifically, no fs, no native modules, no long-running processes. Ask candidates about this explicitly.
"Can you use the fs module inside a Next.js middleware running on the Edge runtime?"
Answer: No. The Edge runtime is a strict subset of the Web APIs. You need to use Web-standard APIs like Request, Response, fetch, crypto, and Headers.
Practical Edge Function Example: Auth Middleware
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth_token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Optionally decode JWT on the edge without Node crypto
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/account/:path*"],
};
Strong candidates will note that JWT verification on the edge requires a Web Crypto-compatible library (like jose) rather than jsonwebtoken, which depends on Node.js internals.
Route Handler with Edge Runtime
// app/api/geo/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
const { geo } = request as any;
return new Response(
JSON.stringify({
country: geo?.country ?? "Unknown",
city: geo?.city ?? "Unknown",
}),
{
headers: { "Content-Type": "application/json" },
}
);
}
Section 4: Caching and Revalidation
This is where even experienced developers get tripped up. Next.js 14+ introduced a layered caching system that behaves differently from anything in the Pages Router.
The Four Cache Layers
-
Request Memoization - Within a single render tree, identical
fetchcalls are deduped -
Data Cache - Persisted across requests and deployments, controlled by
next.revalidate - Full Route Cache - Statically rendered routes cached at the CDN level
- Router Cache - Client-side cache of visited RSC payloads
Interview Scenario
"You have a product page that should update its price every 30 seconds but its description can stay cached for a week. How do you implement that?"
Expected answer:
// For price (revalidate every 30s)
const priceData = await fetch("https://api.example.com/price/123", {
next: { revalidate: 30 },
});
// For description (revalidate weekly)
const descData = await fetch("https://api.example.com/description/123", {
next: { revalidate: 604800 },
});
Or use tag-based revalidation:
// Fetching with a cache tag
const res = await fetch("https://api.example.com/products/123", {
next: { tags: ["product-123"] },
});
// In a Server Action or Route Handler, revalidate by tag
import { revalidateTag } from "next/cache";
revalidateTag("product-123");
A weak candidate will suggest adding cache: 'no-store' everywhere "to be safe." That is a performance anti-pattern that removes all caching benefits.
Section 5: Server Actions
Server Actions are one of the most misunderstood modern features. They let you run server-side code directly from a form or event handler without writing a separate API route.
Code Example to Discuss
// app/contact/page.tsx
async function submitContactForm(formData: FormData) {
"use server";
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await saveToDatabase({ name, email });
}
export default function ContactPage() {
return (
<form action={submitContactForm}>
<input name="name" type="text" placeholder="Your name" required />
<input name="email" type="email" placeholder="Your email" required />
<button type="submit">Send</button>
</form>
);
}
Ask: "What is the security implication of using a Server Action exposed to a form?"
A senior candidate will bring up CSRF protection (Next.js handles this automatically for Server Actions), input validation on the server, and rate limiting.
Section 6: Practical Hiring Checklist
Use this table when evaluating candidates after a technical screen.
| Skill Area | Junior | Mid-Level | Senior |
|---|---|---|---|
| App Router file conventions | Knows basics | Confident with parallel/intercepted routes | Can architect complex nested layouts |
| Server vs Client Components | Knows the use client directive |
Understands composition patterns | Optimizes for minimal client bundle |
| Data fetching | Uses useEffect mostly |
Uses async Server Components | Manages all four cache layers intentionally |
| Edge Functions | Has heard of them | Can write basic middleware | Knows runtime limitations and tradeoffs |
| Server Actions | Unfamiliar | Can implement basic forms | Handles optimistic updates and error states |
| Performance | No awareness | Uses Lighthouse | Analyzes bundle, streaming, and TTFB |
Section 7: Red Flags During Interviews
Watch for these patterns. They are consistent signals of a developer who has not kept pace with the framework.
Red flag 1: They describe data fetching as "you have to use getServerSideProps or fetch on the client." This tells you they are not using the App Router at all.
Red flag 2: They cannot explain why you would choose Node.js runtime over Edge runtime for a specific route. They treat edge as simply "faster" without understanding the constraints.
Red flag 3: They add "use client" to every component "just in case." This completely defeats the purpose of Server Components and bloats the client bundle.
Red flag 4: They have no opinion on streaming. A developer working with modern Next.js should know about <Suspense> boundaries and how they affect time to first byte.
Red flag 5: They cannot explain the difference between a Route Handler and a Server Action and when to use each.
Section 8: What a Good Technical Assessment Looks Like
Rather than whiteboard problems, give candidates a small real-world task:
Task brief (take-home, 3 to 4 hours):
Build a product listing page using the Next.js App Router that:
- Fetches products from a mock API at build time with ISR (revalidate every 60 seconds)
- Has a client-side search filter that does not trigger a server round trip
- Includes a "Add to Wishlist" Server Action that saves to a local JSON file or in-memory store
- Has a middleware that logs each request to the console only in development mode
- Has at least one route on the Edge runtime
This task reveals everything. You see their folder structure, their understanding of the client/server boundary, their cache strategy, and how they think about the Edge runtime.
Final Thoughts
The difference between a developer who knows Next.js and one who truly understands the modern stack is not about years of experience. It is about whether they have shipped real features using the App Router, wrestled with caching bugs, optimized Server Component boundaries, and deployed something to the edge.
Your hiring process should reflect that reality. Ask about tradeoffs, not just syntax. Ask about production problems, not just toy examples. Ask them to write code that reveals their mental model of the server/client boundary.
When you hire developers with this level of fluency, your Next.js application becomes faster, cheaper to run, and far easier to scale. When you do not, you end up with a codebase that looks like it uses the App Router but behaves like the Pages Router in disguise.
Top comments (0)