Branch

Next.js App router

Human dan

I'm interviewing for a senior engineer position and would like to do some light interview prep. The team I'd be joining is migrating and modernizing a legacy application. That application is used by organizations around the world, so it is key that the migration is smooth and painless.

Responsibilities

  • Support legacy data migration efforts
  • Prepare and validate customer data for migration
  • Execute and monitor data load activities
  • Investigate and resolve migration related activities
  • Implement fixes and enhancements to migration tooling and related application code
  • Deliver minor user interface improvements to support migration workflows.
  • Conduct code reviews and ensure code quality standards
  • Solve complex technical problems and identify practical solutions
  • Collaborate with stakeholders on migration priorities and outcomes

Qualifications

  • 4+ years of software dev experience
  • Python (Django): Strong experience building and maintaining server-side applications. Comfortable with Django management commands, ORM bulk operations, transactions, and service-layer patterns. Experience with the data migration domain (ETL, data mapping, checkpointed jobs) is a plus.
  • Data migration and ETL: Hands-on experience moving structured data between systems. Familiarity with XML/CSV parsing, metadata schemas, controlled vocabularies Controlled Vocabularies, and bulk database loads. Django and ETL Experience with data collections, DAM, or library/archive metadata is strongly preferred. Dam and libary/archive metadata
  • PostgreSQL: Solid understanding of relational databases, JSON/JSONB fields, upserts, and debugging data integrity issues at scale.
    2 branches
  • TypeScript / React: Working proficiency for minor UI updates in Next.js App router codebase. Next.js App router You are here
This branch begins here Next.js App router
Human dan

Show me an example of a production Next.js App router codebase. How would I go about adding/removing routes?

AI gpt-5.6-terra

A production Next.js App Router codebase is typically organized around the app/ directory, where folders map to URL segments and special files define behavior.

Example structure:

src/
  app/
    layout.tsx
    page.tsx
    globals.css

    (marketing)/
      about/
        page.tsx
      pricing/
        page.tsx

    dashboard/
      layout.tsx
      page.tsx
      migrations/
        page.tsx
        [migrationId]/
          page.tsx
          loading.tsx
          error.tsx

    api/
      migrations/
        route.ts
      migrations/
        [migrationId]/
          route.ts

    login/
      page.tsx

    not-found.tsx

  components/
    migrations/
      MigrationTable.tsx
      MigrationStatusBadge.tsx
      MigrationDetail.tsx
    ui/
      Button.tsx
      Table.tsx

  lib/
    api/
      migrations.ts
    auth/
      session.ts
    db/
      client.ts
    validation/
      migrationSchemas.ts

  types/
    migration.ts

Key App Router conventions

File/folder Purpose
app/page.tsx Page at /
app/layout.tsx Shared layout for a route tree
app/dashboard/page.tsx Page at /dashboard
app/migrations/[id]/page.tsx Dynamic route, e.g. /migrations/123
loading.tsx Loading UI while a route segment is rendered
error.tsx Error boundary for a route segment
not-found.tsx 404 UI
route.ts HTTP endpoint / route handler
(groupName) Route group; organizes files without affecting the URL
_components/ Common convention for private colocated components; underscore prevents it from becoming a route

Example: migration dashboard

A page for listing migrations:

// src/app/dashboard/migrations/page.tsx
import { MigrationTable } from "@/components/migrations/MigrationTable";
import { getMigrations } from "@/lib/api/migrations";

export default async function MigrationsPage() {
  const migrations = await getMigrations();

  return (
    <main>
      <h1>Customer migrations</h1>
      <MigrationTable migrations={migrations} />
    </main>
  );
}

This maps to:

/dashboard/migrations

A detail page uses a dynamic route:

// src/app/dashboard/migrations/[migrationId]/page.tsx
import { notFound } from "next/navigation";
import { getMigration } from "@/lib/api/migrations";

type PageProps = {
  params: Promise<{ migrationId: string }>;
};

export default async function MigrationDetailPage({ params }: PageProps) {
  const { migrationId } = await params;
  const migration = await getMigration(migrationId);

  if (!migration) {
    notFound();
  }

  return (
    <main>
      <h1>{migration.customerName}</h1>
      <p>Status: {migration.status}</p>
      <p>Records processed: {migration.recordsProcessed}</p>
    </main>
  );
}

That supports URLs such as:

/dashboard/migrations/mig_12345

In a real application, authorization should happen before returning data:

import { redirect } from "next/navigation";
import { requireUser } from "@/lib/auth/session";

export default async function MigrationDetailPage({ params }: PageProps) {
  const user = await requireUser();

  if (!user.canManageMigrations) {
    redirect("/dashboard");
  }

  // Fetch only data that user is permitted to view.
}

Adding a route

To add a route, create a folder under app/ and add a page.tsx.

For example, to create:

/dashboard/migrations/new

add:

src/app/dashboard/migrations/new/page.tsx
// src/app/dashboard/migrations/new/page.tsx
import { MigrationCreateForm } from "@/components/migrations/MigrationCreateForm";

export default function NewMigrationPage() {
  return (
    <main>
      <h1>Create migration</h1>
      <MigrationCreateForm />
    </main>
  );
}

A corresponding navigation link might be:

import Link from "next/link";

export function MigrationActions() {
  return (
    <Link href="/dashboard/migrations/new">
      Create migration
    </Link>
  );
}

Use Link rather than a plain anchor tag for internal navigation. It enables client-side navigation and route prefetching.

Adding a nested layout

If all migration pages share tabs, permissions, or a sidebar, add a layout:

// src/app/dashboard/migrations/layout.tsx
import Link from "next/link";

export default function MigrationsLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <section>
      <nav aria-label="Migration navigation">
        <Link href="/dashboard/migrations">All migrations</Link>
        <Link href="/dashboard/migrations/new">New migration</Link>
      </nav>

      {children}
    </section>
  );
}

That layout applies to:

  • /dashboard/migrations
  • /dashboard/migrations/new
  • /dashboard/migrations/[migrationId]

but not unrelated routes such as /dashboard/users.

Adding a route handler / API endpoint

In App Router, API-style endpoints use route.ts, not pages/api.

For example:

src/app/api/migrations/route.ts
// src/app/api/migrations/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { createMigration, listMigrations } from "@/lib/api/migrations";
import { requireUser } from "@/lib/auth/session";

const createMigrationSchema = z.object({
  customerId: z.string().min(1),
  sourceSystem: z.enum(["legacy_catalog", "spreadsheet", "archive_export"]),
});

export async function GET() {
  const user = await requireUser();

  const migrations = await listMigrations({
    organizationId: user.organizationId,
  });

  return NextResponse.json({ data: migrations });
}

export async function POST(request: Request) {
  const user = await requireUser();
  const body = await request.json();

  const parsed = createMigrationSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: "Invalid migration request", details: parsed.error.flatten() },
      { status: 400 },
    );
  }

  const migration = await createMigration({
    ...parsed.data,
    organizationId: user.organizationId,
    createdByUserId: user.id,
  });

  return NextResponse.json({ data: migration }, { status: 201 });
}

This serves:

GET /api/migrations
POST /api/migrations

A dynamic endpoint:

src/app/api/migrations/[migrationId]/route.ts

can support:

GET    /api/migrations/mig_123
PATCH  /api/migrations/mig_123
DELETE /api/migrations/mig_123

Removing a route

Removing a route is mostly a filesystem operation:

  1. Delete the route’s page.tsx folder or file.
  2. Remove links, redirects, tests, documentation, and navigation entries that point to it.
  3. Check for API consumers if deleting a route.ts handler.
  4. Consider compatibility before removing externally bookmarked or integrated routes.

For example, to remove:

/dashboard/migrations/new

delete:

src/app/dashboard/migrations/new/

Then remove links to it:

<Link href="/dashboard/migrations/new">Create migration</Link>

For production applications, abruptly removing a route can create broken bookmarks or integrations. Often it is safer to redirect it first.

// src/app/dashboard/migrations/new/page.tsx
import { redirect } from "next/navigation";

export default function DeprecatedNewMigrationPage() {
  redirect("/dashboard/migrations");
}

For a permanent redirect at the framework/configuration level:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async redirects() {
    return [
      {
        source: "/dashboard/migrations/new",
        destination: "/dashboard/migrations",
        permanent: true,
      },
    ];
  },
};

export default nextConfig;

Use a 308/permanent redirect only when the old URL should never return. Use a temporary redirect while a feature is being migrated or rolled out.

Production route design considerations

For a migration-focused product, I would generally use routes resembling:

/dashboard
/dashboard/migrations
/dashboard/migrations/new
/dashboard/migrations/[migrationId]
/dashboard/migrations/[migrationId]/records
/dashboard/migrations/[migrationId]/validation
/dashboard/migrations/[migrationId]/runs
/dashboard/migrations/[migrationId]/runs/[runId]
/dashboard/migrations/[migrationId]/errors

This corresponds naturally to the domain: a migration has source files, validation results, execution runs, and record-level errors.

A useful convention is to keep route-level code close to the route while keeping reusable business logic outside the UI tree:

app/dashboard/migrations/[migrationId]/
  page.tsx
  loading.tsx
  error.tsx
  _components/
    MigrationSummary.tsx
    MigrationRunList.tsx

lib/migrations/
  service.ts
  queries.ts
  validation.ts
  permissions.ts

The page should orchestrate rendering and authorization; service-layer functions should contain migration-domain logic; database access should be encapsulated rather than scattered through React components.

One important App Router distinction: components are Server Components by default. Add "use client" only where browser interactivity is needed—for example, an upload widget, a filterable table, or a form with local state. That keeps data access and initial rendering on the server by default.

Explore conversation