Next.js web app coding conventions
Apply these conventions to the requested work. Explicit user instructions override any rule or default in this skill. Preserve existing product requirements and integrations; applying this skill does not authorize an unrelated migration or installing every optional dependency.
Before implementation, inspect project instructions, installed versions, and existing conventions. Read relevant installed Next.js documentation when available, especially node_modules/next/dist/docs/; otherwise consult the version-appropriate official documentation. Do not assume framework APIs from another version.
Defaults and conditional features
| Area | Default | Override or activation rule |
|---|---|---|
| Framework | Next.js App Router and strict TypeScript | Honor the project's chosen versions and explicit user choices. |
| Components | Base UI with Tailwind CSS, wrapped in local components | Support Radix, shadcn/ui, or another requested library. Preserve the same component organization. |
| Variants | class-variance-authority and a Tailwind-aware class-merging helper | Reuse an established equivalent. |
| API transport | Axios behind custom APIClient and ServerAPIClient abstractions | An existing custom client, fetch, or another requested transport is valid. Keep contracts independent of transport. |
| Validation | Zod schemas with inferred TypeScript types | Required by default at external data boundaries; change only on explicit user direction. |
| Remote client data | TanStack Query with a shared query wrapper | Reuse a requested equivalent. |
| Shared interactive state | Zustand | Use local React state for state owned by one component. Honor explicit alternatives. |
| Authentication | Provider-neutral email OTP flow when authentication is requested | Follow the selected provider and requested login methods; do not choose or provision a provider implicitly. |
| Cross-tab synchronization | Enabled for relevant shared mutation events | Optional and removable on request. Add events only where cross-tab consistency is useful. |
| Logging and tracing | Follow an existing logging system | If none exists, do not introduce, propose, or mention logging/trace infrastructure unless requested. |
| Mock data and simulation | Off | Add only when the user requests mock data or development/testing simulation. |
| Development tooling | Separate development-only modules | Add tools only when relevant or requested; no prescribed tool or overlay. |
Choose defaults when a decision is needed; do not ask about each default. Do not introduce auth, state stores, synchronization, or infrastructure into features that do not need them.
Required folder structure
Use this organization for new code. Create directories as their responsibilities become necessary rather than filling the project with empty placeholders. In an existing project, avoid moving unrelated files without a migration request.
src/
├── app/ Routes, layouts, API endpoints
├── components/
│ ├── ui/ Shared UI primitives
│ ├── standalone/ Reusable domain components
│ ├── pages/<page>/ Page-specific components
│ └── <feature>/ Feature subsystems
├── controllers/
│ ├── client/<domain>/ Browser API calls
│ └── server/<domain>/ Server-side API calls
├── providers/ React context providers
├── stores/ Zustand stores
├── hooks/ Shared React hooks
├── helpers/ Auth/access and other workflows
├── lib/ API clients, cache, logging, sync
├── db/ Database/Redis clients
├── utils/ Small utilities and constants
└── assets/icons/ Source SVGs
Folder names for infrastructure describe placement, not required services. No database, Redis, cache, or logging stack is implied by this tree.
- Organize domain code beneath these responsibility folders; avoid replacing them with a different top-level architecture.
- Use PascalCase component files,
useSomething.tshooks (.tsxonly when JSX is present), and camelCase operation files such asupdateName.ts. - Use descriptive domain and feature directories. Keep framework filenames such as
page.tsx,layout.tsx, androute.tsunchanged. - Use
index.tsto assemble controller namespaces andindex.tsxas the entry point of a multi-file component. - Use
@/for imports rooted insrc; use relative imports for close internal siblings. Useimport typefor type-only dependencies. - Avoid temporary copies, backup folders, duplicate implementations, and catch-all files with unclear responsibilities.
Routes and component composition
Keep app/ focused on routing, layout composition, request handling, and initial data loading. Use route groups such as (auth), (no-auth), and (settings) when their layout boundaries are useful. A route-group name alone provides no access protection.
Keep pages thin: load initial data, resolve page-level decisions, and render components from components/pages/<page>/ or the relevant feature directory. Server Components are the default; introduce client boundaries for browser state, events, and APIs.
Component placement:
components/ui/: domain-independent primitives and wrappers around the chosen UI library.components/standalone/: reusable domain controls used across pages or features.components/pages/<page>/: components specific to one page.components/<feature>/: larger subsystems shared across routes, such as navigation or a command interface.
Split complex components into focused files. This is a core convention. A substantial component should have a directory with an index.tsx composition entry point and named supporting components, tabs, rows, hooks, and types as needed. Do not accumulate multiple substantial subcomponents and unrelated effects in one file. Split by responsibility rather than an arbitrary line limit; avoid fragmenting trivial markup.
Keep interaction mechanics in feature-local hooks or focused handler components: keyboard navigation, selection, event subscriptions, drag behavior, and view coordination. Move a hook to src/hooks/ when it is shared. Keep input shortcuts scoped so they do not interfere with editable controls; clean up listeners on unmount.
Wrap the primitive library locally and expose composable parts such as Dialog, DialogTrigger, DialogContent, and DialogTitle. Preserve accessible semantics, focus behavior, and the actual library's composition API. Do not copy Radix-specific props into Base UI wrappers without checking compatibility.
Type wrapper props from the underlying primitive or native element and add only needed custom props. Use typed variants for size, emphasis, and state. Merge consumer classes predictably and preserve ref support according to the installed React/library APIs.
Use semantic CSS variables connected to Tailwind utilities. Keep theme values centralized and component variants local. This skill prescribes styling organization, not a visual theme, spacing scale, or product design.
Compose relevant context providers in app/providers.tsx; keep their implementations in providers/. Keep browser-only providers behind an explicit client boundary and avoid mixing development tool initialization with normal provider responsibilities.
Controllers and HTTP clients
Use one operation per file under controllers/client/<domain>/ or controllers/server/<domain>/. Assemble nested namespace objects through index.ts files, for example:
ClientController.org.members.list()
ClientController.profile.updateName(input)
ServerController.org.info()
Controllers provide typed operations and endpoint construction. Components should call controllers rather than assembling URLs or transport configuration. Controllers remain thin; put reusable workflow logic in helpers/ and infrastructure in lib/.
Keep browser and server HTTP clients separate:
lib/APIClient.ts: browser-safe transport, query/body serialization, response handling, and cancellation support.lib/ServerAPIClient.ts: server-only transport, trusted base URL, and request-scoped credentials/session context.- Keep server secrets and server-only dependencies out of browser imports. Never keep per-user credentials in a shared mutable singleton.
- When server controllers call application endpoints, forward only the required credentials to trusted destinations. Do not trust a caller-supplied identity header as authentication.
- Normalize transport errors, non-2xx responses, and application errors consistently. Preserve meaningful status/code information. Do not turn failed requests into successful empty data.
- Support
AbortSignalthrough controller and transport layers. Cancellation should not trigger error notifications or retries.
API schemas, contracts, and handlers
Zod schemas are the default source of truth for inputs. Define specific schemas for body, query, and path parameters, including formats, allowed values, length/range limits, optionality, and deliberate coercion. Validate before using external values. Avoid z.any(), broad records, casts, and non-null assertions as substitutes for contracts. Treat unknown keys deliberately for each boundary.
Colocate method-specific schemas and exported request/query/response types with the route by default. Infer request types with z.infer; define explicit response contracts and use response schemas where runtime validation is needed. Reuse these types in controllers and components instead of re-declaring them. If runtime contracts must be shared with browser code, or route-module export constraints require it, use an adjacent schema.ts or types.ts and type-only imports so server implementation is never pulled into the client bundle.
Use a consistent discriminated response envelope:
type APIResponse<T> =
| { success: true; data: T; error: null }
| {
success: false;
data: null;
error: { code: string; message: string; details?: unknown };
};
Narrow details to explicit safe validation/error detail types where used. Keep errors and HTTP statuses consistent across all branches. Do not expose internal exceptions or stack traces in production responses.
Organize multi-method routes into method-specific schema/type sections and functions such as get_request and post_request. Export handlers through withAPIRequestHandler() in lib/api/APIRequestHandler.ts.
Maintain lib/api/error/ with an APIError class and a domain-organized, typed catalog of error codes, messages, and statuses. The shared handler maps validation failures, domain errors, and unexpected failures to the same response contract. Integrate logging only under the logging condition above.
Extract repeated organization lookup, identity resolution, query parsing, and access workflows into helpers/. Keep small pure transformations and constants in utils/. Split larger integrations into focused workflow files; the route should orchestrate these steps rather than contain the entire integration.
Data fetching and state
Load initial page data through server controllers when appropriate and pass typed, serializable props into interactive components. Use Promise.all for independent requests; preserve ordering for dependent operations.
Use TanStack Query for remote client data. Provide shared defaults and a useApiQuery wrapper that unwraps the API envelope, propagates errors, and forwards the query cancellation signal. Query keys must include relevant identity/resource/filter inputs. Scope server-side query clients per request; invalidate or update affected query entries after mutations.
Use feature-specific Zustand stores for shared interactive state such as selection, filters, panels, and command state. Subscribe to individual slices or use shallow selection for grouped values. Keep setters/actions explicit and reset state when its owning resource changes. Avoid maintaining a second independent copy of remote data in Zustand when Query already owns it.
Use local state for component-owned inputs, dirty state, validation messages, and saving state. Prevent duplicate submissions and preserve user input on failures. There is no required form library.
Cancel superseded searches/filter requests and abort on cleanup. Guard against stale completions updating a new resource. Only classify actual cancellation as cancellation; do not silently swallow network failures.
For large lists, apply virtualization and pagination when useful, with loading, empty, error, retry, and end-of-list states. Reset pagination and selection appropriately when the resource/filter changes. Keep keyboard navigation consistent with virtualized rows.
Optional cross-tab synchronization
Enable synchronization for relevant shared mutations by default unless the user disables it. Do not add it to isolated state without a synchronization need.
Use a typed event registry in lib/local-sync/, domain event schemas beneath events/, and a shared hooks/useLocalSync.ts hook. Infer payload types from Zod and validate both outgoing and incoming messages.
A storage-event implementation should notify other tabs and same-tab listeners, use transient event payloads rather than persistent application state, and clean up listeners. An existing equivalent transport is acceptable. Prevent echo loops and duplicate handling; do not synchronize secrets. Events should update or invalidate the canonical state owner. Cross-tab events are UI coordination, not authorization or guaranteed delivery.
Authentication and authorization
Apply this section when auth is in scope. Keep the implementation provider-neutral and adapt to the selected service or custom backend.
- For email OTP authentication, separate send-code, verify-code, and sign-out handlers. Define precise Zod schemas for each request and explicit success/error contracts. Respect the selected backend's token format and session lifecycle.
- Expose client session state, loading state, and refresh behavior through
providers/SessionProvider.tsxand a typed session hook. Subscribe and unsubscribe to session changes through an adapter appropriate to the backend. - Separate authentication (verified identity/session) from authorization (permission to act on a resource).
- Keep shared request checks in proxy helpers when supported by the installed framework, and enforce access at the server operation boundary. UI hiding, route groups, and session context are not access controls.
- Centralize organization/membership/resource access helpers. Scope resource queries to the verified user's permitted organization or resources; cover each applicable HTTP method.
- Centralize typed role/permission constants. Use the roles needed by the product rather than imposing a fixed role model.
- Keep protected layouts and API checks active. Never bypass authentication as a shortcut for development or mock data.
Errors and conditional supporting systems
Use route-level error.tsx/global-error.tsx and reusable component error boundaries where local recovery is useful. Provide meaningful retry or recovery behavior. Use existing error reporting only when present or requested; do not claim errors were reported unless reporting actually occurred.
Logging condition: when a logging system already exists, use its abstraction and conventions. When the user requests a new system, place shared infrastructure in lib/ and configure the chosen provider. Otherwise omit logging/tracing setup and suggestions from implementation and delivery. Never include credentials or sensitive payloads in logs.
Mock condition: only when explicitly requested, colocate fixtures such as mock.json beside the relevant API route or feature. Keep fixtures consistent with real contracts and gate mock execution explicitly to the intended development/test mode. Avoid unconditional early mock returns or changes to real auth enforcement.
Development tooling: place development UI in components/dev/ and other development-only integrations in src/dev/ or purpose-specific scripts. Keep these separate from product components and workflows and gate execution to development. The skill does not require a particular inspector, overlay, or tool.
Completion check
Verify the touched feature follows the folder structure, complex components are split by responsibility, contracts remain specific, and client/server dependencies stay separated. Check relevant loading/error/cancellation paths and authorization behavior when affected. Run the project's appropriate checks for the change. Do not introduce unrequested optional systems or refactor unrelated features to satisfy this checklist.