418 lines
15 KiB
Markdown
418 lines
15 KiB
Markdown
# Cross-Layer Thinking Guide
|
|
|
|
> **Purpose**: Pre-implementation checklist for features that span multiple layers.
|
|
>
|
|
> **Core Principle**: 30 minutes of thinking saves 3 hours of debugging.
|
|
|
|
---
|
|
|
|
## When to Use This Guide
|
|
|
|
Use this guide when your feature:
|
|
|
|
- Touches 3+ layers (Server Component, Client Component, oRPC, Database)
|
|
- Involves data transformation between layers
|
|
- Has real-time or event-driven components
|
|
- Receives data from external sources (APIs, webhooks, file uploads)
|
|
|
|
---
|
|
|
|
## Pre-Implementation Checklist
|
|
|
|
Before writing code, answer these questions:
|
|
|
|
### 1. Layer Identification
|
|
|
|
**Which layers does this feature touch?**
|
|
|
|
- [ ] Server Components (RSC - data fetching, static rendering)
|
|
- [ ] Client Components (interactivity, browser APIs, React hooks)
|
|
- [ ] API Routes / oRPC Procedures (validation, business logic)
|
|
- [ ] Middleware (auth checks, redirects, header manipulation)
|
|
- [ ] Database (Drizzle ORM queries, migrations)
|
|
- [ ] Server Actions (form handling, progressive enhancement)
|
|
- [ ] External Services (third-party APIs, webhooks)
|
|
|
|
### 2. Data Flow Direction
|
|
|
|
**How does data flow?**
|
|
|
|
```
|
|
Read Flow: DB -> Drizzle -> oRPC Handler -> API Response -> React Query -> Component -> UI
|
|
Write Flow: UI -> Form/Action -> oRPC Mutation -> Handler -> Drizzle -> DB
|
|
SSR Flow: DB -> Drizzle -> oRPC Handler -> Server Component -> HTML -> Client Hydration
|
|
```
|
|
|
|
- [ ] Read-only (data flows from DB to UI)
|
|
- [ ] Write-only (data flows from UI to DB)
|
|
- [ ] Bidirectional (both directions)
|
|
- [ ] Server-rendered (data fetched in Server Components)
|
|
- [ ] Client-fetched (data fetched via React Query in Client Components)
|
|
|
|
### 3. Data Format at Each Layer
|
|
|
|
**What format is the data at each boundary?**
|
|
|
|
| Layer | Format | Example |
|
|
| ---------------- | ----------------------- | ----------------------------------------------- |
|
|
| Database | SQL types | `TEXT`, `INTEGER`, `TIMESTAMP`, `JSONB` |
|
|
| Drizzle ORM | TypeScript types | `string`, `number`, `Date`, `Record<>` |
|
|
| oRPC Handler | Zod-validated objects | `{ id: string, createdAt: Date }` |
|
|
| oRPC Response | Serialized JSON | `{ id: "abc", createdAt: "2024-01-01T..." }` |
|
|
| React Query | Cached response | Same as oRPC response (deserialized) |
|
|
| Server Component | Props (must serialize) | No functions, no Date objects, no class instances |
|
|
| Client Component | React state | Component props, hook return values |
|
|
| UI | Rendered output | HTML, Tailwind-styled elements |
|
|
|
|
### 3.1 Serialization Boundary (CRITICAL!)
|
|
|
|
**Design Principle**: Data crossing the Server/Client Component boundary must be serializable.
|
|
|
|
| Serializable (OK) | NOT Serializable (WILL BREAK) |
|
|
| ----------------------- | --------------------------------- |
|
|
| `string`, `number` | `Date` objects |
|
|
| `boolean`, `null` | `Map`, `Set` |
|
|
| Plain objects, arrays | Functions, class instances |
|
|
| `undefined` (as absent) | `BigInt`, `Symbol` |
|
|
|
|
**Common serialization trap**:
|
|
|
|
```typescript
|
|
// BAD - Date objects don't serialize across RSC boundary
|
|
async function ItemPage() {
|
|
const item = await orpcClient.items.get({ itemId: "123" });
|
|
// item.createdAt might be a Date object from Drizzle
|
|
return <ClientItem item={item} />; // Date becomes string or breaks!
|
|
}
|
|
|
|
// GOOD - Convert to serializable format before passing to Client Component
|
|
async function ItemPage() {
|
|
const item = await orpcClient.items.get({ itemId: "123" });
|
|
return <ClientItem item={{
|
|
...item,
|
|
createdAt: item.createdAt.toISOString(), // Explicit string conversion
|
|
}} />;
|
|
}
|
|
```
|
|
|
|
### 4. Data Transformation Points
|
|
|
|
**Where does format change? Who is responsible?**
|
|
|
|
| From | To | Transformer | Location |
|
|
| ----------------- | ------------------- | ------------------- | -------------------------- |
|
|
| DB timestamp | JS Date | Drizzle ORM | Automatic |
|
|
| JS Date | ISO string | oRPC serialization | API response |
|
|
| ISO string | Display string | React component | UI layer |
|
|
| User input | Validated data | Zod schema | oRPC input validation |
|
|
| JSONB column | TypeScript object | Drizzle + cast | Query layer |
|
|
|
|
### 5. Boundary Questions (Critical!)
|
|
|
|
For each layer boundary, ask:
|
|
|
|
**RSC / Client Component Boundary:**
|
|
|
|
- What data is the Server Component passing as props?
|
|
- Is all of it serializable? (no functions, no Date objects, no Maps)
|
|
- Could this data be fetched directly in the Client Component via React Query instead?
|
|
- Does the Client Component need to refetch or mutate this data?
|
|
|
|
**Client Component / oRPC Boundary:**
|
|
|
|
- What format does the oRPC response return?
|
|
- How does React Query cache and deserialize it?
|
|
- What happens if the response format changes?
|
|
- Are query keys consistent for cache invalidation?
|
|
|
|
**oRPC Handler / Database Boundary:**
|
|
|
|
- Are timestamps handled consistently? (ISO strings vs Date objects)
|
|
- Are IDs strings or numbers?
|
|
- What about null vs undefined?
|
|
- Does Drizzle transform types automatically?
|
|
- Are JSONB columns properly cast?
|
|
|
|
**Middleware / Route Boundary:**
|
|
|
|
- Is auth checked in middleware, oRPC procedure, or both?
|
|
- What happens if middleware redirects but the API call continues?
|
|
- Are headers properly forwarded in SSR context?
|
|
|
|
### 6. Authentication Context
|
|
|
|
**Where is auth available?**
|
|
|
|
| Layer | Auth Method | Notes |
|
|
| ---------------- | -------------------------------------- | --------------------------------------- |
|
|
| Middleware | `getSession()` from headers/cookies | Runs before route handler |
|
|
| Server Component | `getSession()` or `auth()` helper | Can redirect on the server |
|
|
| Client Component | `useSession()` hook | May need loading state |
|
|
| oRPC Procedure | `protectedProcedure` middleware | Throws UNAUTHORIZED if no session |
|
|
| API Route | `getSession()` from request headers | Manual check needed |
|
|
|
|
**Common auth pitfall**:
|
|
|
|
```typescript
|
|
// BAD - Auth checked in middleware but not in oRPC procedure
|
|
// If someone calls the API directly, auth is bypassed!
|
|
export const middleware = NextResponse.next(); // auth check here
|
|
export const getSecret = publicProcedure.handler(...); // no auth check!
|
|
|
|
// GOOD - Auth in oRPC procedure (always enforced)
|
|
export const getSecret = protectedProcedure.handler(...);
|
|
```
|
|
|
|
### 7. Edge Cases
|
|
|
|
- [ ] What if the data is empty/null?
|
|
- [ ] What if the database query fails?
|
|
- [ ] What if the oRPC call times out?
|
|
- [ ] What if a referenced entity doesn't exist?
|
|
- [ ] What if the user navigates away mid-mutation?
|
|
- [ ] What if React Query returns stale data?
|
|
- [ ] What if the user's session expires mid-operation?
|
|
- [ ] What if the same mutation fires twice (double-click)?
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern A: Server Component Data Fetch
|
|
|
|
**Layers**: Server Component -> oRPC Client -> Handler -> Database
|
|
|
|
**Data Flow**:
|
|
|
|
```
|
|
1. Server Component: Calls oRPC client directly (server-side)
|
|
2. oRPC Handler: Validates auth, queries database
|
|
3. Drizzle: Returns typed results
|
|
4. Server Component: Renders HTML with data
|
|
5. Client: Receives pre-rendered HTML
|
|
```
|
|
|
|
**Common Issues**:
|
|
|
|
- **Serialization**: Server Components can render Date objects directly, but cannot pass them as props to Client Components
|
|
- **No cache**: Server-side oRPC calls bypass React Query cache; consider prefetching
|
|
- **Waterfall**: Sequential server-side calls create request waterfalls; use `Promise.all` for parallel fetching
|
|
|
|
### Pattern B: Client Component with React Query
|
|
|
|
**Layers**: Client Component -> React Query -> oRPC Client -> Handler -> Database
|
|
|
|
**Data Flow**:
|
|
|
|
```
|
|
1. Client Component: Mounts, triggers useQuery
|
|
2. React Query: Checks cache, calls oRPC client if stale
|
|
3. oRPC Client: Sends HTTP request to API route
|
|
4. oRPC Handler: Validates input/auth, queries DB
|
|
5. Response: JSON back through React Query
|
|
6. Client Component: Re-renders with data
|
|
```
|
|
|
|
**Common Issues**:
|
|
|
|
- **Loading states**: Must handle `isLoading`, `isError`, `isPending` properly
|
|
- **Stale data**: Configure `staleTime` and `gcTime` appropriately
|
|
- **Cache invalidation**: Use `orpc.xxx.key()` for consistent invalidation after mutations
|
|
- **Enabled flag**: Disable queries when required parameters are missing
|
|
|
|
### Pattern C: Mutation with Optimistic Update
|
|
|
|
**Layers**: Client Component -> useMutation -> oRPC Client -> Handler -> Database
|
|
|
|
**Data Flow**:
|
|
|
|
```
|
|
1. User: Triggers action (click, form submit)
|
|
2. onMutate: Optimistically update React Query cache
|
|
3. oRPC Client: Sends mutation request
|
|
4. Handler: Validates, writes to DB
|
|
5. onSuccess: Invalidate related queries
|
|
6. onError: Rollback optimistic update from snapshot
|
|
```
|
|
|
|
**Common Issues**:
|
|
|
|
- **Rollback complexity**: Must snapshot all affected queries before optimistic update
|
|
- **Type safety**: Cache manipulation needs explicit type annotations
|
|
- **Race conditions**: Cancel outgoing refetches before optimistic update (`cancelQueries`)
|
|
- **Partial failures**: Batch operations may partially succeed
|
|
|
|
### Pattern D: Server Action (Form Handling)
|
|
|
|
**Layers**: Form -> Server Action -> oRPC Client / DB -> Revalidate
|
|
|
|
**Data Flow**:
|
|
|
|
```
|
|
1. User: Submits form
|
|
2. Server Action: Receives FormData, validates
|
|
3. Action: Calls oRPC client or DB directly
|
|
4. Action: Calls revalidatePath/revalidateTag
|
|
5. Page: Re-renders with updated data
|
|
```
|
|
|
|
**Common Issues**:
|
|
|
|
- **Progressive enhancement**: Forms work without JS when using Server Actions
|
|
- **Validation**: Validate on both client (UX) and server (security)
|
|
- **Redirect vs revalidate**: Choose the right post-action behavior
|
|
- **Error handling**: Server Action errors need proper error boundaries
|
|
|
|
### Pattern E: Middleware + API Route Auth
|
|
|
|
**Layers**: Request -> Middleware -> API Route / oRPC -> Handler
|
|
|
|
**Data Flow**:
|
|
|
|
```
|
|
1. Request: Arrives at Next.js server
|
|
2. Middleware: Checks auth, may redirect to login
|
|
3. API Route: Handles oRPC request
|
|
4. oRPC Middleware: Validates session (protectedProcedure)
|
|
5. Handler: Executes business logic
|
|
```
|
|
|
|
**Common Issues**:
|
|
|
|
- **Double auth check**: Middleware protects pages, oRPC protects API; both are needed
|
|
- **Header forwarding**: SSR requests must forward cookies/headers to oRPC client
|
|
- **Middleware scope**: Don't run auth middleware on public assets or API routes that handle their own auth
|
|
|
|
---
|
|
|
|
## Lessons from Common Bugs
|
|
|
|
| Bug | Root Cause | Prevention |
|
|
| ------------------------------- | --------------------------------------------------------- | --------------------------------------------------- |
|
|
| `Date` props break hydration | Date objects passed from Server to Client Component | Convert to ISO string before passing as props |
|
|
| Stale data after mutation | Forgot to invalidate React Query cache | Always invalidate with `orpc.xxx.key()` in onSuccess |
|
|
| Auth bypass on API | Auth only in middleware, not in oRPC procedure | Always use `protectedProcedure` for protected data |
|
|
| `BigInt` serialization error | Database returns BigInt, JSON.stringify fails | Cast to number or string before response |
|
|
| Query fires with null ID | `enabled` flag not set on conditional queries | Always guard with `enabled: !!requiredParam` |
|
|
| Cache key mismatch | Manual query key doesn't match oRPC generated key | Always use `orpc.xxx.key()` or `orpc.xxx.queryKey()` |
|
|
| N+1 queries in handler | Fetching related data in a loop | Use `inArray()` for batch queries |
|
|
| Hydration mismatch | Server and client render different output (e.g., locale) | Ensure consistent data between server and client |
|
|
| Headers not forwarded in SSR | oRPC client doesn't forward cookies in server context | Configure client to forward headers in SSR mode |
|
|
|
|
---
|
|
|
|
## Checklist Template
|
|
|
|
Copy this for your feature:
|
|
|
|
```markdown
|
|
## Feature: [Name]
|
|
|
|
### Layers Involved
|
|
|
|
- [ ] Server Component
|
|
- [ ] Client Component
|
|
- [ ] oRPC Procedure
|
|
- [ ] Middleware
|
|
- [ ] Database
|
|
- [ ] Server Action
|
|
- [ ] External Service
|
|
|
|
### Data Flow
|
|
|
|
[Describe the flow]
|
|
|
|
### Format at Each Layer
|
|
|
|
| Layer | Format |
|
|
| ----- | ------ |
|
|
| ... | ... |
|
|
|
|
### Transformation Points
|
|
|
|
| From | To | Who |
|
|
| ---- | --- | --- |
|
|
| ... | ... | ... |
|
|
|
|
### Auth Strategy
|
|
|
|
- Middleware: [yes/no, what it checks]
|
|
- oRPC: [publicProcedure/protectedProcedure/adminProcedure]
|
|
|
|
### Edge Cases Considered
|
|
|
|
- [ ] Empty/null data
|
|
- [ ] Invalid format / serialization
|
|
- [ ] Operation failure / timeout
|
|
- [ ] User cancellation / navigation
|
|
- [ ] Session expiry mid-operation
|
|
- [ ] Double submission
|
|
```
|
|
|
|
---
|
|
|
|
## Cross-Layer Review Mindset
|
|
|
|
### The Comparison Trap
|
|
|
|
**Wrong thinking**: "This line wasn't changed, so it must be correct."
|
|
|
|
```
|
|
Comparison thinking (surface level):
|
|
Before: new Date() -> After: new Date() -> "No change, must be fine"
|
|
|
|
Global thinking (design level):
|
|
Design intent: ISO strings across RSC boundary -> Current: Date object -> "This is a bug"
|
|
```
|
|
|
|
**Key insight**: Review validates "system state is correct", not just "change is correct".
|
|
|
|
### Data Outlet Checklist
|
|
|
|
Every review must cover ALL data outlets:
|
|
|
|
```
|
|
Data Outlets:
|
|
|-- oRPC Response (handler -> client)
|
|
|-- Server Component Props (RSC -> Client Component)
|
|
|-- React Query Cache (shared across components)
|
|
|-- URL State (nuqs, searchParams)
|
|
|-- Server Action Return (action -> form)
|
|
|-- Any external interface
|
|
```
|
|
|
|
Ask: **"Is the format correct at EACH outlet?"**
|
|
|
|
### Review Three Questions
|
|
|
|
Before finishing any cross-layer review:
|
|
|
|
1. **Outlet Question**: Have I checked ALL data outlets, not just the "core" one?
|
|
2. **Design Question**: Does existing code match design principles? (Not "is the change correct?")
|
|
3. **Checklist Question**: Could my checklist itself be wrong?
|
|
|
|
### Validation vs Verification
|
|
|
|
| Approach | Focus | Risk |
|
|
| --------------- | ---------------------------- | ------------------------------------ |
|
|
| **Incremental** | "Is this change correct?" | Misses pre-existing bugs |
|
|
| **Global** | "Is the system correct now?" | More thorough, catches legacy issues |
|
|
|
|
Always prefer global verification for cross-layer features.
|
|
|
|
---
|
|
|
|
## When Things Go Wrong
|
|
|
|
If you encounter a cross-layer bug:
|
|
|
|
1. **Identify the boundary** - Where exactly does it fail?
|
|
2. **Log at boundaries** - Add logging before and after each transformation
|
|
3. **Check assumptions** - What format did you expect vs what you got?
|
|
4. **Test in isolation** - Can you reproduce with a simple test case?
|
|
5. **Document the fix** - Add to "Lessons from Common Bugs" table
|
|
|
|
---
|
|
|
|
**Language**: All documentation should be written in **English**.
|