Skip to content

Instantly share code, notes, and snippets.

@qpleple
Created January 31, 2026 13:14
Show Gist options
  • Select an option

  • Save qpleple/d5b2e93f1276c7f3fba253427a3d1d1c to your computer and use it in GitHub Desktop.

Select an option

Save qpleple/d5b2e93f1276c7f3fba253427a3d1d1c to your computer and use it in GitHub Desktop.
Exemple de standards de code pour une application Next.js

Architecture Standards

Stack

  • Framework: Next.js App Router only (app/), no pages/
  • Language: TypeScript everywhere, no JS
  • Runtime: only Node
  • Package manager: pnpm, enforced via CI

Repository Structure

├── app/                    # routes, layouts, metadata
├── components/             # shared UI (framework-agnostic)
├── features/<feature>/     # feature modules (product code lives here)
├── lib/                    # shared utilities (no React)
├── server/                 # server-only (db, repositories, auth)
├── styles/                 # global styles
├── types/                  # shared types (avoid if type lives in feature)
└── pub                     # static assets

Rules:

  • All product logic in features/* unless truly global
  • No "utils" dumping ground: feature-scoped or lib/ with clear ownership

Routing

  • Route segments for areas: app/(marketing)/..., app/(app)/...
  • Dynamic routes: [id] only, avoid deep nesting
  • Route handlers: app/api/.../route.ts
  • Redirects: redirect() in server components, useRouter() in client
  • Internal links: <Link> only, never <a>

Server vs Client Components

Default: Everything is Server Component unless it needs state, effects, browser APIs, or event handlers.

Rules:

  • "use client" at top of file, must be rare
  • Client components cannot import from server/* (enforce with ESLint)
  • Data fetching in server components or server actions, not useEffect

Data Fetching

  • Prefer server-side fetching in route-level components, pass data down
  • Use fetch() with explicit caching:
// Default: no caching
fetch(url, { cache: 'no-store' })

// With caching: document why
fetch(url, { next: { revalidate: 60, tags: ['invoices'] } })
  • Tag invalidation for revalidation
  • No ad-hoc caching layers in components

Server Actions

  • Use for mutations (forms, button actions)
  • Placement: features/<feature>/actions.ts or app/<route>/actions.ts
  • Naming: verbNounAction
// features/invoices/actions.ts
'use server'

export async function createInvoiceAction(data: CreateInvoiceInput) {
  const parsed = createInvoiceSchema.safeParse(data)
  if (!parsed.success) {
    return { error: 'VALIDATION_ERROR' }
  }
  // ...
  return { data: invoice }
}

Rules:

  • Validate input (Zod)
  • Return typed results
  • Never expose internal errors, map to friendly codes

API Routes

Use when needed for:

  • Third-party webhooks
  • Non-React clients
  • Streaming endpoints

Response shape:

// Always JSON with { data, error }
return Response.json({ data: result })
return Response.json({ error: 'NOT_FOUND' }, { status: 404 })
  • Use proper status codes, never "200 with error"

State Management

Priority:

  1. URL state + server state (default)
  2. Local component state
  3. Context (truly global UI only)
  4. Library (pick one: Zustand/Jotai/Redux, ban others)
  • Remote server state via React Query or avoided entirely

Authentication & Authorization

  • Auth centralized: server/auth/*
  • Authorization rules in feature server layer, not UI
  • Never trust client state for permissions
  • All server actions and APIs enforce authorization

Data Access Layer

server/
├── db/              # client, migrations
├── repositories/    # queries (small, typed, composable)
└── services/        # domain operations, transactions
  • No raw DB calls in React components
  • Transactions managed in service layer

Environment Variables

Single typed module: server/env.ts

// server/env.ts
import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string(),
  NEXT_PUBLIC_APP_URL: z.string().optional(),
})

export const env = envSchema.parse(process.env)

Naming:

  • Server-only: FOO_BAR
  • Client-exposed: NEXT_PUBLIC_FOO_BAR (only when strictly necessary)
  • No direct process.env.X outside env module

Style Standards

TypeScript

  • strict: true
  • No any, use unknown and narrow
  • Use type for unions and simple objects, interface for extendable public shapes
  • Shared API payloads: Zod schemas with inferred types
const userSchema = z.object({
  id: z.string(),
  email: z.string().email(),
})
type User = z.infer<typeof userSchema>

Naming Conventions

Element Convention Example
Components PascalCase InvoiceList.tsx
Hooks camelCase with use useInvoice.ts
Utilities camelCase formatCurrency.ts
Server actions verbNounAction createInvoiceAction
Booleans is/has/can/should prefix isLoading, hasAccess

Component Conventions

Location:

  • src/components/ - reusable, generic
  • src/features/<feature>/components/ - feature-specific

Rules:

  • Explicit props types over React.FC
  • No "God components", split by responsibility
// Prefer
type ButtonProps = {
  variant: 'primary' | 'secondary'
  children: React.ReactNode
}

export function Button({ variant, children }: ButtonProps) {
  // ...
}

Styling

Tailwind: no CSS modules, only Tailwind + small globals.css

Rules:

  • No inline styles except dynamic values
  • Design tokens (colors, spacing, typography) in one place

Forms

  • Prefer native <form> + server actions
  • Server is source of truth for validation
  • Client validation is optional UX
  • Consistent field errors shape across app

Error Handling

  • Use error.tsx and not-found.tsx per route segment
  • Async server operations: controlled error mapping, stable error type
  • Server logs include request correlation id
  • No console.log in production (lint error)

Accessibility

  • Minimum: keyboard navigation + aria labels for interactive elements
  • Semantic HTML first
  • Color contrast enforced by linting or review checklist

Performance

  • Images use next/image unless documented reason
  • No client-side waterfalls: don't fetch in effects for initial render
  • No large dependencies without review
  • Lazy-load heavy client components

Security

  • All input validated on server
  • CSRF protection aligned with auth approach
  • Secrets never in client code
  • Dependabot enabled and monitored

Linting & Formatting

  • ESLint + Prettier mandatory, run in CI
  • No formatting debates in PRs
  • Import sorting enforced
  • Absolute imports: @/features/...

Dependencies

New dependencies require:

  • Justification
  • Size/perf consideration
  • Maintenance check

Rules:

  • Prefer "boring" libraries with wide adoption
  • Ban duplicate libraries in same category

Testing Standards

Testing Pyramid

Layer Tool Scope
Unit Vitest/Jest Pure logic
Component Testing Library UI components
E2E Playwright Critical user paths

Minimum per feature:

  • Unit tests for domain logic
  • One E2E for critical user path

Test location: Pick one and enforce:

  • Next to code: Button.test.tsx
  • Or in __tests__/

Git Workflow

Branch naming:

  • feat/<feature>
  • fix/<issue>
  • chore/<task>

Merge strategy: Squash merge

Documentation

Feature README (src/features/<feature>/README.md):

  • Purpose
  • Main flows
  • Key components
  • Data contracts

Architecture Decision Records: docs/adr/ (short ADR format)

# ADR-001: Use Zustand for client state

## Status
Accepted

## Context
Need lightweight client state for UI-only concerns.

## Decision
Use Zustand over Redux/Jotai.

## Consequences
- Simpler API, smaller bundle
- Team must learn new patterns
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment