Skip to content

Contributing Nextjs Guidelines

Vladimir Kromm edited this page Sep 17, 2026 · 5 revisions

Next.js Coding Guidelines

Code-level conventions for Next.js/React/TypeScript in this repo. Companion to Contributing-UI-Guidelines (styling, components) and Contributing (process). Referenced from CLAUDE.md — binding for agents and humans alike.

Rendering & data flow

  • Public, crawlable pages (e.g. the preps catalog) are strict SSR for SEO: page.tsx fetches everything via server actions (src/actions/*) and passes it down through props — no client-side fetching, the first render already has the content.
  • Authenticated/personal pages (prep creation, dashboard) can relax this: a self-contained client component may fetch its own data via a server action instead of threading it through props from page.tsx, when that measurably cuts prop-drilling or complexity. Not the default — reach for it only when the plain page → props flow is actually getting in the way.
  • A component that fetches its own data uses a route-level loading.tsx for the loading state, not a hand-rolled spinner (see #79).
  • Mutations go through server actions, not API routes. Mutating server actions call revalidatePath — the client receives fresh data through props on the next render, no router.refresh() or mirrored useState needed.
  • Client components receive server data through props and use it directly. Do not mirror props in useState + useEffect to sync them — that duplicates the data flow Next.js already manages. Only hold local state for values the component mutates independently of the server (UI toggles, in-progress pipeline output, form input).
  • Shareable/bookmarkable page state (filters, tab, progress) lives in the URL query string, not client state.
  • Internal navigation: Link for links, useRouter for imperative navigation, redirect for server-side. Never a bare <a> for an internal route.
  • Images: always Image from next/image, never <img>.

File & naming conventions

  • src/actions/<entity>.ts — kebab-case, "use server", named exports. Export each action's return type alongside it. Owns the request boundary: auth, input validation, cache revalidation. Calls a service when there's real business logic; calls a repository directly only for a pure passthrough (fetch, list, insert with no logic attached).
  • src/services/<entity>.ts — business logic that doesn't belong in a single repository call: calculations, orchestrating several repositories, business-specific mapping. Named exports, no framework imports (no "use server", no next/cache) — services stay callable outside a request.
  • src/repositories/<entity>.ts — Prisma queries only, no business logic.
  • src/components/<domain>/Component.tsx — PascalCase, named export, "use client" when interactive.
  • src/components/ui/* — business-agnostic primitives; check here before writing a new one (see Contributing-UI-Guidelines).
  • Route-specific client components live alongside their route's page.tsx (e.g. app/catalog/CatalogPage.tsx). New pages follow the page.tsx + server-action flow.

Types

  • One pair per entity: EntityBase (list/reference fields) and Entity extends EntityBase (detail view, full relations). Extend an existing pair instead of forking a near-duplicate for a 1–2 field difference (no PrepForCard / PrepForModal).
  • Fixed value sets are enums, never string literals. In this codebase that means an as const array plus a derived type (export const DISCIPLINES = [...] as const; type Discipline = (typeof DISCIPLINES)[number]) — not TypeScript's enum keyword. Pair it with a type-guard function (value is Discipline) wherever an untyped string needs to become one, instead of asserting with as.
  • Never cast with as. Narrow with a type guard, a validated parse, or a schema (zod) instead — an as asserts safety the compiler didn't check, and the two common spots it creeps in (a UI library callback handing back a raw string, a URL query param) are exactly where the value is least trustworthy. The only exception is an isolated file whose entire job is wrapping a library's own types — not a call site inside otherwise-normal feature code.

Control flow

  • Fail fast: invalid state throws, it doesn't fall back to a default or an empty result unless naturally needed. A catch that swallows and returns [] or null hides the bug it should surface.
  • No nested if-else — use guard clauses / early returns. If a function still needs nesting after that, extract a function.
  • No silent default case on an unsupported enum/union value — handle it explicitly or throw.
  • Minimize optional fields. An optional field is a claim that both the present and absent shape are valid — don't make it optional just because one code path happens not to set it.

Database queries

  • Optimize for readable and maintainable, not maximally efficient. Two simple, obvious Prisma queries beat one clever query that's hard to read, debug, or change later.
  • Straightforward joins are welcome. Reach for a nested query or subquery only when the simple version is a measured problem, not on the chance it might shave off a few milliseconds.

Dependencies

  • Prefer a framework built-in (useState, FormEvent) over a library for anything simple enough to hand-roll.
  • Check package.json for an existing dependency that already does this before adding a new one.

Clone this wiki locally