5.6 KiB
5.6 KiB
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/
├── <route>/
│ ├── page.tsx # Server Component by default. ≤200 LOC.
│ ├── layout.tsx
│ ├── _components/ # Private folder; not routable. Colocated UI.
│ │ └── <Component>.tsx # Each file ≤300 LOC.
│ ├── _hooks/ # Client hooks for this route.
│ ├── _server/ # Server actions, data loaders for this route.
│ └── loading.tsx / error.tsx
├── api/
│ └── <domain>/route.ts # Thin handler. Delegates to lib/server/<domain>/.
lib/
├── <domain>/ # Pure helpers, types, schemas (zod). Reusable.
└── server/<domain>/ # 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— neverparse(throws and bypasses error handling). - Delegate to
lib/server/<domain>/. No business logic inroute.ts. - Always return
NextResponse.json(..., { status }). Let the framework's error boundary handle unexpected errors — don't wrap the entire handler intry/catch.
// app/api/<domain>/route.ts (≤40 LOC)
import { NextRequest, NextResponse } from 'next/server';
import { mySchema } from '@/lib/schemas/<domain>';
import { myService } from '@/lib/server/<domain>';
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
useEffectfor server-renderable data. - State management: prefer URL state (
searchParams) and Server Components over global stores.
Types
lib/sdk/types.tsis being split intolib/sdk/types/<domain>.ts. Mirror backend domain boundaries.- All API DTOs are zod schemas; infer types via
z.infer. - No
any. Noas unknown as. If you reach for it, the type is wrong. - Always use
import type { Foo }for type-only imports. - Never use
astype assertions except when bridging external data at a boundary (add a comment explaining why). - No
@ts-ignore.@ts-expect-erroronly 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.
// 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/<domain>.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/reactrenderHook. - 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 onapp/.
Tooling
tsc --noEmitclean (strict mode,noUncheckedIndexedAccess: true).- ESLint with
@typescript-eslint,eslint-config-next, type-aware rules on. prettier.next buildclean. No// @ts-ignore.// @ts-expect-erroronly with a comment explaining why.
Performance
- Use
next/dynamicfor heavy client-only components. - Image:
next/imagewith explicit width/height. - Avoid waterfalls —
Promise.allfor parallel data fetches in Server Components.
What you may NOT do
- Put business logic in a
page.tsxorroute.ts. - Reach across module boundaries (e.g.
admin-complianceimporting fromdeveloper-portal). - Use
dangerouslySetInnerHTMLwithout 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
...propsonto 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.