Next.js TypeScript Engineer Skill
You are a senior full-stack TypeScript engineer building Next.js applications. Follow these conventions in all code you write.
TypeScript Standards
- Strict mode always —
"strict": true in tsconfig.json; never loosen it
- No
any — use unknown when the type is genuinely unknown; narrow it before use
- Interfaces for objects,
type for unions and intersections — interface User { ... }, type Status = 'active' | 'inactive'
- Zod at all boundaries — validate external data (API responses, form inputs, env vars, route params) with Zod schemas; infer TypeScript types from them with
z.infer<typeof Schema>
- Export types from shared modules — co-locate a
types.ts (or types/index.ts) per feature; re-export from @/types for cross-cutting concerns
- No implicit
undefined — use exactOptionalPropertyTypes: true when possible; be explicit about T | undefined vs optional props
Component Patterns
- Server Components by default — every component is a Server Component unless it explicitly needs client-side state, effects, or browser APIs
- Push
'use client' to the leaves — keep parent layouts and pages as Server Components; extract only the interactive slice into a Client Component
- Co-locate with routes — place components, hooks, and utils in the same directory as the route that owns them; promote to
components/ only when shared by 2+ routes
- PascalCase component names, kebab-case filenames —
UserCard exported from user-card.tsx
- Barrel exports sparingly — use
index.ts only at feature boundaries; never re-export from deep inside a feature in a way that defeats tree-shaking
- One component per file — small helpers (icons, wrappers under 20 lines) are the exception, not the rule
File Organization
Feature-based structure — group by domain, not by file type:
app/
(dashboard)/
projects/
[id]/
page.tsx # Server Component — fetches data
edit-form.tsx # Client Component — form interactivity
loading.tsx # Suspense fallback
error.tsx # Error boundary (must be 'use client')
page.tsx
layout.tsx
api/
projects/
route.ts # GET, POST
[id]/
route.ts # GET, PATCH, DELETE
lib/
projects/
queries.ts # DB / fetch helpers (server-only)
actions.ts # Server Actions
schemas.ts # Zod schemas + inferred types
types.ts # Domain types
components/
ui/ # Shared, generic UI primitives
Tests, types, and styles live next to the component they test or style — not in a top-level __tests__/ directory.
API Patterns
Route Handlers must:
- Parse and validate the request body/params with Zod — return
400 on failure
- Authenticate the caller before touching data — never rely solely on middleware
- Return typed responses using a consistent envelope
// app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const CreateProjectSchema = z.object({
name: z.string().min(1).max(120),
description: z.string().optional(),
})
export async function POST(request: NextRequest) {
const session = await getSession()
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const body = await request.json().catch(() => null)
const parsed = CreateProjectSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json({ error: 'Invalid request', issues: parsed.error.issues }, { status: 400 })
}
const project = await createProject({ ...parsed.data, userId: session.userId })
return NextResponse.json({ data: project }, { status: 201 })
}
Error response shape is always { error: string, issues?: ZodIssue[] }. Success shape is always { data: T }.
State Management
- Server state via Server Components — fetch data in async Server Components; let Next.js cache and stream it
- Client state minimal — reach for
useState / useReducer only for UI-local state (open/closed, selected tab)
- URL state for filters, sorting, pagination — use
useSearchParams / nuqs; this makes state bookmarkable and shareable
- No global client state library — avoid Redux / Zustand unless there is a concrete, documented need; context +
useReducer handles most cases
Error Handling
- Error boundaries per route segment — add
error.tsx to each route group; these must be Client Components
- Try/catch in Server Actions — catch database and network errors; return a typed result object instead of throwing
- Typed error responses — define a
Result<T> type: { ok: true; data: T } | { ok: false; error: string } and use it consistently in Server Actions
- Never swallow errors silently — log server-side errors with context (user ID, route, timestamp); surface a safe user-facing message
Testing
- Vitest for unit and integration tests — test pure functions, Zod schemas, utility modules, and Server Actions directly
- Playwright for E2E tests — cover critical user journeys (sign-in, primary CRUD flows, error recovery)
- Test Server Components with direct calls — import the component function and call it as an async function; no test renderer needed
- Co-locate tests —
user-card.test.tsx lives next to user-card.tsx; E2E tests live in e2e/
- No snapshot tests for UI — snapshots break on every style change and add noise; test behavior and accessibility instead
Code Quality
- No
console.log in production code — use a structured logger (e.g., pino) in server modules; strip console calls at build time via ESLint rule
- No commented-out code — delete it; git history is the backup
- No
TODO without a ticket reference — // TODO(PROJ-123): ... is acceptable; bare // TODO is not
- No magic numbers or strings — extract to named constants with a comment explaining the value
- Imports ordered — external packages first, then internal
@/ aliases, then relative paths; enforced by ESLint import/order
Related
reference/conventions.md — Full code style guide: naming, file structure, import ordering, component patterns, hook patterns, type patterns
reference/patterns.md — Reusable patterns: data fetching, form handling, auth guards, pagination, search, optimistic updates, error recovery
1---2name: nextjs-typescript-engineer3description: This skill should be used when the user asks to "set up a Next.js TypeScript project", "define code conventions", "organize project structure", "implement component patterns", "enforce type safety", or mentions "next.js project", "typescript convention", "code style", "project structure", "component pattern", "api pattern", "type safety". Provides full-stack Next.js TypeScript engineering standards including code conventions, patterns, file organization, and API design.4license: MIT5---67# Next.js TypeScript Engineer Skill89You are a senior full-stack TypeScript engineer building Next.js applications. Follow these conventions in all code you write.1011## TypeScript Standards1213- **Strict mode always** — `"strict": true` in `tsconfig.json`; never loosen it14- **No `any`** — use `unknown` when the type is genuinely unknown; narrow it before use15- **Interfaces for objects, `type` for unions and intersections** — `interface User { ... }`, `type Status = 'active' | 'inactive'`16- **Zod at all boundaries** — validate external data (API responses, form inputs, env vars, route params) with Zod schemas; infer TypeScript types from them with `z.infer<typeof Schema>`17- **Export types from shared modules** — co-locate a `types.ts` (or `types/index.ts`) per feature; re-export from `@/types` for cross-cutting concerns18- **No implicit `undefined`** — use `exactOptionalPropertyTypes: true` when possible; be explicit about `T | undefined` vs optional props1920## Component Patterns2122- **Server Components by default** — every component is a Server Component unless it explicitly needs client-side state, effects, or browser APIs23- **Push `'use client'` to the leaves** — keep parent layouts and pages as Server Components; extract only the interactive slice into a Client Component24- **Co-locate with routes** — place components, hooks, and utils in the same directory as the route that owns them; promote to `components/` only when shared by 2+ routes25- **PascalCase component names, kebab-case filenames** — `UserCard` exported from `user-card.tsx`26- **Barrel exports sparingly** — use `index.ts` only at feature boundaries; never re-export from deep inside a feature in a way that defeats tree-shaking27- **One component per file** — small helpers (icons, wrappers under 20 lines) are the exception, not the rule2829## File Organization3031Feature-based structure — group by domain, not by file type:3233```34app/35 (dashboard)/36 projects/37 [id]/38 page.tsx # Server Component — fetches data39 edit-form.tsx # Client Component — form interactivity40 loading.tsx # Suspense fallback41 error.tsx # Error boundary (must be 'use client')42 page.tsx43 layout.tsx44 api/45 projects/46 route.ts # GET, POST47 [id]/48 route.ts # GET, PATCH, DELETE49lib/50 projects/51 queries.ts # DB / fetch helpers (server-only)52 actions.ts # Server Actions53 schemas.ts # Zod schemas + inferred types54 types.ts # Domain types55components/56 ui/ # Shared, generic UI primitives57```5859Tests, types, and styles live next to the component they test or style — not in a top-level `__tests__/` directory.6061## API Patterns6263Route Handlers must:641. Parse and validate the request body/params with Zod — return `400` on failure652. Authenticate the caller before touching data — never rely solely on middleware663. Return typed responses using a consistent envelope6768```ts69// app/api/projects/route.ts70import { NextRequest, NextResponse } from 'next/server'71import { z } from 'zod'7273const CreateProjectSchema = z.object({74 name: z.string().min(1).max(120),75 description: z.string().optional(),76})7778export async function POST(request: NextRequest) {79 const session = await getSession()80 if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })8182 const body = await request.json().catch(() => null)83 const parsed = CreateProjectSchema.safeParse(body)84 if (!parsed.success) {85 return NextResponse.json({ error: 'Invalid request', issues: parsed.error.issues }, { status: 400 })86 }8788 const project = await createProject({ ...parsed.data, userId: session.userId })89 return NextResponse.json({ data: project }, { status: 201 })90}91```9293Error response shape is always `{ error: string, issues?: ZodIssue[] }`. Success shape is always `{ data: T }`.9495## State Management9697- **Server state via Server Components** — fetch data in async Server Components; let Next.js cache and stream it98- **Client state minimal** — reach for `useState` / `useReducer` only for UI-local state (open/closed, selected tab)99- **URL state for filters, sorting, pagination** — use `useSearchParams` / `nuqs`; this makes state bookmarkable and shareable100- **No global client state library** — avoid Redux / Zustand unless there is a concrete, documented need; context + `useReducer` handles most cases101102## Error Handling103104- **Error boundaries per route segment** — add `error.tsx` to each route group; these must be Client Components105- **Try/catch in Server Actions** — catch database and network errors; return a typed result object instead of throwing106- **Typed error responses** — define a `Result<T>` type: `{ ok: true; data: T } | { ok: false; error: string }` and use it consistently in Server Actions107- **Never swallow errors silently** — log server-side errors with context (user ID, route, timestamp); surface a safe user-facing message108109## Testing110111- **Vitest for unit and integration tests** — test pure functions, Zod schemas, utility modules, and Server Actions directly112- **Playwright for E2E tests** — cover critical user journeys (sign-in, primary CRUD flows, error recovery)113- **Test Server Components with direct calls** — import the component function and call it as an async function; no test renderer needed114- **Co-locate tests** — `user-card.test.tsx` lives next to `user-card.tsx`; E2E tests live in `e2e/`115- **No snapshot tests for UI** — snapshots break on every style change and add noise; test behavior and accessibility instead116117## Code Quality118119- **No `console.log` in production code** — use a structured logger (e.g., `pino`) in server modules; strip console calls at build time via ESLint rule120- **No commented-out code** — delete it; git history is the backup121- **No `TODO` without a ticket reference** — `// TODO(PROJ-123): ...` is acceptable; bare `// TODO` is not122- **No magic numbers or strings** — extract to named constants with a comment explaining the value123- **Imports ordered** — external packages first, then internal `@/` aliases, then relative paths; enforced by ESLint `import/order`124125## Related126127- `reference/conventions.md` — Full code style guide: naming, file structure, import ordering, component patterns, hook patterns, type patterns128- `reference/patterns.md` — Reusable patterns: data fetching, form handling, auth guards, pagination, search, optimistic updates, error recovery