Volume 4

Q216: TypeScript with Next.js App Router Boundaries

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to know whether you can use TypeScript well inside a modern Next.js App Router codebase.

They check Server Components, Client Components, route params, metadata, Route Handlers, server actions, serializable props, environment variables, and how type boundaries affect bundle size and performance.

Short Interview Answer

In the Next.js App Router, I keep data fetching and server-only types in Server Components or Route Handlers, then pass small serializable props into Client Components. TypeScript helps define route params, API responses, metadata, form inputs, and client component contracts. I avoid pushing server-only objects, functions, database clients, or secrets across the client boundary because that creates runtime issues and can increase bundle cost.

Detailed Interview Answer

Next.js App Router architecture is boundary-driven.

The main boundaries are:

server component to client component
route handler to caller
server action to form/client
static generation to runtime
environment config to application code
MDX content to rendered UI

TypeScript should make those boundaries explicit.

Server Components By Default

In the App Router, components are Server Components unless marked otherwise.

export default async function QuestionPage() {
  const question = await loadQuestion();
  return <QuestionReader question={question} />;
}

Server Components can use:

file system
database clients
server-only SDKs
private environment variables
async data fetching

They do not ship their implementation to the browser.

Client Component Boundary

Client Components need "use client".

"use client";

type ProgressButtonProps = {
  questionId: string;
  title: string;
};

export function ProgressButton({ questionId, title }: ProgressButtonProps) {
  return <button>{questionId}: {title}</button>;
}

Props passed from server to client should be serializable.

Avoid passing:

functions
class instances
database records with methods
Dates unless normalized
Maps and Sets unless converted
server-only clients

Normalize data before the boundary.

Serializable View Models

Create view models for client components.

type QuestionViewModel = {
  id: string;
  title: string;
  slug: string;
  estimatedMinutes: number;
};

This keeps the client component stable.

It also reduces shipped data.

Do not pass a full server domain object when the client needs five fields.

Route Params

Next.js route params should be typed at the page boundary.

type QuestionPageProps = {
  params: Promise<{ slug: string }>;
};

export default async function QuestionPage({ params }: QuestionPageProps) {
  const { slug } = await params;
  return <main>{slug}</main>;
}

The exact shape should match the route segment.

For catch-all routes, type arrays.

type DocsPageProps = {
  params: Promise<{ slug: string[] }>;
};

Route Handlers

Route Handlers are API boundaries.

export async function POST(request: Request) {
  const body: unknown = await request.json();
  const parsed = parsePayload(body);

  if (!parsed.ok) {
    return Response.json({ error: parsed.error }, { status: 400 });
  }

  return Response.json({ ok: true });
}

Do not cast request bodies directly to trusted types.

Request JSON is runtime data.

Metadata

Metadata should be typed through framework contracts.

import type { Metadata } from "next";

export async function generateMetadata(): Promise<Metadata> {
  return {
    title: "Learning Studio",
    description: "Senior frontend preparation",
  };
}

Explicit return types help catch invalid metadata shape.

Environment Variables

Types can describe environment variables, but runtime validation still matters.

function requireServerEnv(name: string) {
  const value = process.env[name];

  if (!value) {
    throw new Error(`Missing ${name}`);
  }

  return value;
}

Client-exposed variables should be intentionally prefixed and limited.

Never leak secrets into Client Components.

Server Actions

Server actions need typed inputs and outputs.

type SaveProgressResult =
  | { ok: true }
  | { ok: false; message: string };

export async function saveProgress(formData: FormData): Promise<SaveProgressResult> {
  "use server";

  const questionId = formData.get("questionId");

  if (typeof questionId !== "string") {
    return { ok: false, message: "Missing question id" };
  }

  return { ok: true };
}

FormData is untyped runtime data, so parse it.

Bundle Implications

TypeScript types disappear, but boundaries affect JavaScript bundles.

If a file is marked "use client", its imports may enter the client graph.

That means:

keep client files small
avoid importing server utilities into client components
move heavy helpers behind server components
pass serializable data down
lazy-load rare client features

Good type boundaries can support good performance boundaries.

Common Mistakes

Common mistakes include:

putting use client too high in the tree
passing full server objects to client components
casting request JSON directly
typing env vars without validating runtime values
importing server-only modules into client files
forgetting route params can be async framework inputs

The biggest mistake is blurring the server/client boundary.

Senior Trade-Offs

I prefer:

Server Components for content and data
small Client Components for interactivity
view models at boundaries
runtime validation for request bodies
typed results for actions
explicit metadata and route param types

This keeps the app safe, fast, and easier to reason about.

Interview Framing

In an interview, say:

I use TypeScript to make App Router boundaries explicit. Server code owns data and secrets, client components receive small serializable props, and runtime inputs like route bodies or FormData are parsed before use.

Revision Notes

  • Server Components are the default in App Router.
  • Client Component props should be serializable.
  • Route params, metadata, actions, and handlers should have clear types.
  • Request bodies and FormData still need runtime parsing.
  • "use client" affects bundle boundaries.
  • Type boundaries and performance boundaries often align.

Follow-Up Questions

  • Why should Client Component props be serializable?
  • How can "use client" increase bundle cost?
  • How would you type a Route Handler request body?
  • Why are environment variable types not enough?
  • How would you model a server action result?

How I Would Answer This In A Real Interview

I would explain that Next.js App Router has architectural boundaries, and TypeScript should make those boundaries visible. I keep data fetching, secrets, and heavy work on the server. I pass small typed view models into Client Components. I parse FormData and request JSON at runtime, type route params and metadata explicitly, and watch "use client" placement because it changes what ships to the browser.