Engineering Handbook
Frontend

Next.js

Next.js best practices for routing, rendering, data flow, and operations.

Next.js

Next.js is our primary React framework for application structure, routing, rendering modes, and deployment-ready conventions.

What it is

Next.js provides app routing, server/client rendering patterns, server actions, middleware, and build tooling for modern React applications.

Best practices

Why we use it

  • Strong defaults for production-ready React apps.
  • App Router enables layout composition and route-level ownership.
  • Integrates cleanly with server-side rendering and API routes.

Setup in this repo

  • Use App Router patterns and keep route segments domain-oriented.
  • Keep shared app shell/layout concerns in top-level layout files.
  • Use environment variables through documented Next.js conventions.

Team conventions

  • Default to server components where interaction is not required.
  • Use client components only for interactivity and browser-only APIs.
  • Keep API route contracts explicit and typed.
  • Centralize reusable data-access logic instead of duplicating fetch code.
  • Pair server rendering with TanStack Query client cache when hybrid flows are needed.

Error handling and reliability

  • Use route-level error boundaries where appropriate.
  • Handle not-found and empty states explicitly.
  • Validate server inputs and guard against invalid query params.

Testing and validation

  • Verify route behavior, loading states, and API contracts.
  • Add integration tests for middleware/auth redirects on critical paths.
  • Run lint/build checks before merge for route and bundle regressions.

Abstractions and anti-patterns

  • Avoid turning every component into a client component by default.
  • Avoid duplicating fetch logic across server and client paths.
  • Keep route structure meaningful; do not over-nest without ownership benefit.

Example

// app/projects/page.tsx
export default async function ProjectsPage() {
  const res = await fetch("https://example.com/api/projects", {
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Failed to load projects");

  const projects = (await res.json()) as Array<{ id: string; name: string }>;

  return (
    <main>
      <h1 className="text-xl font-semibold">Projects</h1>
      <ul className="mt-4 space-y-2">
        {projects.map((project) => (
          <li key={project.id}>{project.name}</li>
        ))}
      </ul>
    </main>
  );
}

Common pitfalls

  • Marking high-level layouts as client components unnecessarily.
  • Inconsistent cache strategies across similar routes.
  • Mixing business logic directly inside route files without reuse boundaries.
  • Missing error/loading/empty states in route segments.

References

Internal

External

On this page