# AGENTS.typescript.md — TypeScript / Next.js Conventions Applies to: `admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`, `dsms-node/` (where applicable). ## Layered architecture (Next.js 15 App Router) ``` app/ ├── / │ ├── page.tsx # Server Component by default. ≤200 LOC. │ ├── layout.tsx │ ├── _components/ # Private folder; not routable. Colocated UI. │ │ └── .tsx # Each file ≤300 LOC. │ ├── _hooks/ # Client hooks for this route. │ ├── _server/ # Server actions, data loaders for this route. │ └── loading.tsx / error.tsx ├── api/ │ └── /route.ts # Thin handler. Delegates to lib/server//. lib/ ├── / # Pure helpers, types, schemas (zod). Reusable. └── server// # Server-only logic; uses "server-only" import. components/ # Truly shared, app-wide components. ``` **Server vs Client:** Default is Server Component. Add `"use client"` only when you need state, effects, or browser APIs. Push the boundary as deep as possible. ## API routes (route.ts) - One handler per HTTP method, ≤40 LOC. - Validate input with zod `safeParse` — never `parse` (throws and bypasses error handling). - Delegate to `lib/server//`. No business logic in `route.ts`. - Always return `NextResponse.json(..., { status })`. Let the framework's error boundary handle unexpected errors — don't wrap the entire handler in `try/catch`. ```typescript // app/api//route.ts (≤40 LOC) import { NextRequest, NextResponse } from 'next/server'; import { mySchema } from '@/lib/schemas/'; import { myService } from '@/lib/server/'; export async function POST(req: NextRequest) { const body = mySchema.safeParse(await req.json()); if (!body.success) return NextResponse.json({ error: body.error }, { status: 400 }); const result = await myService.create(body.data); return NextResponse.json(result, { status: 201 }); } ``` ## Page components - Pages >300 lines must be split into colocated `_components/`. - Server Components fetch data; pass plain objects to Client Components. - No data fetching in `useEffect` for server-renderable data. - State management: prefer URL state (`searchParams`) and Server Components over global stores. ## Types - `lib/sdk/types.ts` is being split into `lib/sdk/types/.ts`. Mirror backend domain boundaries. - All API DTOs are zod schemas; infer types via `z.infer`. - No `any`. No `as unknown as`. If you reach for it, the type is wrong. - Always use `import type { Foo }` for type-only imports. - Never use `as` type assertions except when bridging external data at a boundary (add a comment explaining why). - No `@ts-ignore`. `@ts-expect-error` only with a comment explaining the suppression. ## Barrel re-export pattern `lib/sdk/types.ts` is a barrel — it re-exports from domain-specific files. **Do not add new types directly to it.** ```typescript // lib/sdk/types.ts (barrel — DO NOT ADD NEW TYPES HERE) export * from './types/enums'; export * from './types/company-profile'; // ... etc. // New types go in lib/sdk/types/.ts ``` - When splitting an oversized file, keep the original as a thin barrel so existing imports don't break. - New code imports directly from the specific module (e.g. `import type { CompanyProfile } from '@/lib/sdk/types/company-profile'`), not the barrel. ## Server vs Client components Default is Server Component. Add `"use client"` only when required: | Need | Pattern | |------|---------| | Data fetching only | Server Component (no directive) | | `useState` / `useEffect` | Client Component (`"use client"`) | | Browser API | Client Component | | Event handlers | Client Component | - Pass only serializable props from Server → Client Components (no functions, no class instances). - Never add `"use client"` to a layout or page just because one child needs it — extract the client part into a `_components/` file. ## Tests - Unit: **Vitest** (`*.test.ts`/`*.test.tsx`), colocated. - Hooks: `@testing-library/react` `renderHook`. - E2E: **Playwright** (`tests/e2e/`), one spec per top-level page, smoke happy path minimum. - Snapshot tests sparingly — only for stable output (CSV, JSON-LD). - Coverage target: 70% on `lib/`, smoke coverage on `app/`. ## Tooling - `tsc --noEmit` clean (strict mode, `noUncheckedIndexedAccess: true`). - ESLint with `@typescript-eslint`, `eslint-config-next`, type-aware rules on. - `prettier`. - `next build` clean. No `// @ts-ignore`. `// @ts-expect-error` only with a comment explaining why. ## Before every push — MANDATORY Run all three steps for every affected service (`admin-compliance/`, `developer-portal/`) before pushing. CI runs the same checks and will fail if you skip this. ```bash cd admin-compliance # or developer-portal # 1. Build — catches type errors and module resolution failures npm run build # 2. Lint npx tsc --noEmit npx eslint . --max-warnings 0 # 3. Tests npm test ``` All three must exit 0. Do not push if any step fails. ## Performance - Use `next/dynamic` for heavy client-only components. - Image: `next/image` with explicit width/height. - Avoid waterfalls — `Promise.all` for parallel data fetches in Server Components. ## What you may NOT do - Put business logic in a `page.tsx` or `route.ts`. - Reach across module boundaries (e.g. `admin-compliance` importing from `developer-portal`). - Use `dangerouslySetInnerHTML` without DOMPurify sanitization. - Call internal backend APIs directly from Client Components — use Server Components or API routes as a proxy. - Add `"use client"` to a layout or page just because one child needs it — extract the client part. - Spread `...props` onto a DOM element without filtering the props first (type error risk). - Change a public API route's path/method/schema without updating SDK consumers in the same change. - Create a file >500 lines. - Disable a lint or type rule globally to silence a finding — fix the root cause.