docs: add Trellis planning and project specs
This commit is contained in:
417
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
417
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# 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**.
|
||||
122
.trellis/spec/guides/index.md
Normal file
122
.trellis/spec/guides/index.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Thinking Guides for Next.js Full-Stack Projects
|
||||
|
||||
> **Purpose**: Systematic thinking guides to catch issues before they become bugs.
|
||||
>
|
||||
> **Core Philosophy**: 30 minutes of thinking saves 3 hours of debugging.
|
||||
|
||||
---
|
||||
|
||||
## Why Thinking Guides?
|
||||
|
||||
**Most bugs and tech debt come from "didn't think of that"**, not from lack of skill:
|
||||
|
||||
- Didn't think about what happens at layer boundaries -> cross-layer bugs
|
||||
- Didn't think about code patterns repeating -> duplicated code everywhere
|
||||
- Didn't think about edge cases -> runtime errors
|
||||
- Didn't think about future maintainers -> unreadable code
|
||||
|
||||
These guides help you **ask the right questions before coding**.
|
||||
|
||||
---
|
||||
|
||||
## Available Thinking Guides
|
||||
|
||||
| Guide | Purpose | When to Use |
|
||||
| ----------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
|
||||
| [Cross-Layer Thinking](./cross-layer-thinking-guide.md) | Think through data flow across layers | Before implementing features that span 3+ layers |
|
||||
| [Pre-Implementation Checklist](./pre-implementation-checklist.md) | Verify readiness before coding | Before starting any feature implementation |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: When to Use Which Guide
|
||||
|
||||
### Cross-Layer Issues
|
||||
|
||||
Use [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) when:
|
||||
|
||||
- [ ] Feature touches 3+ layers (Server Component, Client Component, oRPC, Database)
|
||||
- [ ] Data format changes between layers
|
||||
- [ ] Multiple consumers need the same data
|
||||
- [ ] You're not sure where to put some logic
|
||||
- [ ] Integrates with external services or third-party APIs
|
||||
|
||||
### Before Writing Code
|
||||
|
||||
Use [Pre-Implementation Checklist](./pre-implementation-checklist.md) when:
|
||||
|
||||
- [ ] About to add a constant or config value
|
||||
- [ ] About to implement new logic
|
||||
- [ ] About to define a type or Zod schema
|
||||
- [ ] About to create a component or hook
|
||||
- [ ] About to add an oRPC procedure
|
||||
- [ ] Feels like you've seen similar code before
|
||||
|
||||
---
|
||||
|
||||
## The Pre-Modification Rule (CRITICAL)
|
||||
|
||||
> **Before changing ANY value, ALWAYS search first!**
|
||||
|
||||
```bash
|
||||
# Search for the value you're about to change
|
||||
rg "value_to_change" --type ts
|
||||
|
||||
# Check how many files define this value
|
||||
rg "CONFIG_NAME" --type ts -c
|
||||
```
|
||||
|
||||
This single habit prevents most "forgot to update X" bugs.
|
||||
|
||||
---
|
||||
|
||||
## Next.js-Specific Layers
|
||||
|
||||
In Next.js full-stack projects with oRPC and Drizzle, these are the typical layers:
|
||||
|
||||
```
|
||||
Server Components (RSC - data fetching, static rendering)
|
||||
|
|
||||
v
|
||||
Client Components ('use client' - interactivity, React Query)
|
||||
|
|
||||
v
|
||||
API Routes / oRPC Router (type-safe RPC, middleware, validation)
|
||||
|
|
||||
v
|
||||
Service / Business Logic (shared utilities, domain rules)
|
||||
|
|
||||
v
|
||||
Database Layer (Drizzle ORM, PostgreSQL, migrations)
|
||||
```
|
||||
|
||||
Each boundary is a potential source of bugs due to:
|
||||
|
||||
- **Serialization** - Only serializable data crosses the RSC/Client boundary (no functions, no Date objects, no Maps)
|
||||
- **Type mismatches** - Zod schemas on oRPC may not match what the frontend expects
|
||||
- **Auth context** - Session availability differs between Server Components, API routes, and middleware
|
||||
- **Rendering mode** - Server Components vs Client Components have different capabilities and constraints
|
||||
- **Async timing** - React Query caching, stale data, and race conditions
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Search Before Write** - Always search for existing patterns before creating new ones
|
||||
2. **Think Before Code** - 5 minutes of checklist saves 50 minutes of debugging
|
||||
3. **Document Assumptions** - Make implicit assumptions explicit
|
||||
4. **Verify All Layers** - Changes often need updates in multiple places
|
||||
5. **Learn From Bugs** - Add lessons to these guides after fixing non-trivial bugs
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a new "didn't think of that" moment? Add it:
|
||||
|
||||
1. If it's a **general thinking pattern** -> Add to existing guide or create new one
|
||||
2. If it caused a bug -> Add to "Lessons Learned" section in the relevant guide
|
||||
3. If it's **project-specific** -> Create a separate project-specific guide
|
||||
|
||||
---
|
||||
|
||||
**Language**: All documentation should be written in **English**.
|
||||
289
.trellis/spec/guides/pre-implementation-checklist.md
Normal file
289
.trellis/spec/guides/pre-implementation-checklist.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Pre-Implementation Checklist
|
||||
|
||||
> **Purpose**: Ask the right questions **before** writing code to avoid common architectural mistakes.
|
||||
|
||||
---
|
||||
|
||||
## Why This Checklist?
|
||||
|
||||
Most code quality issues aren't caught during implementation--they're **designed in** from the start:
|
||||
|
||||
| Problem | Root Cause | Cost |
|
||||
| -------------------------------------- | --------------------------------------------- | -------------------------- |
|
||||
| Constants duplicated across 5 files | Didn't ask "will this be used elsewhere?" | Refactoring + testing |
|
||||
| Same logic repeated in multiple hooks | Didn't ask "does this pattern exist?" | Creating abstraction later |
|
||||
| Cross-layer type mismatches | Didn't ask "who else consumes this?" | Debugging + fixing |
|
||||
| Zod schema redefined in frontend | Didn't ask "is this type already exported?" | Inconsistent validation |
|
||||
| oRPC procedure duplicates existing one | Didn't ask "does a similar endpoint exist?" | API surface bloat |
|
||||
|
||||
**This checklist catches these issues before they become code.**
|
||||
|
||||
---
|
||||
|
||||
## The Checklist
|
||||
|
||||
### 1. Constants & Configuration
|
||||
|
||||
Before adding any constant or config value:
|
||||
|
||||
- [ ] **Cross-package usage?** Will this value be used in both frontend app and API package?
|
||||
- If yes -> Put in a shared package (e.g., `@your-app/utils` or `@your-app/config`)
|
||||
- Example: `MAX_UPLOAD_SIZE` used by both file upload UI and oRPC validation
|
||||
|
||||
- [ ] **Multiple consumers?** Will this value be used in 2+ files within the same package?
|
||||
- If yes -> Put in a shared constants file for that package
|
||||
- Example: Don't define `DEBOUNCE_MS = 300` in each hook file
|
||||
|
||||
- [ ] **Magic number?** Is this a hardcoded value that could change?
|
||||
- If yes -> Extract to named constant with comment explaining why
|
||||
- Example: `PAGINATION_LIMIT: 50 // oRPC default page size`
|
||||
|
||||
- [ ] **Environment-dependent?** Does this differ between dev/staging/production?
|
||||
- If yes -> Use environment variables with proper validation
|
||||
- Example: API URLs, feature flags, third-party API keys
|
||||
|
||||
### 2. Logic & Patterns
|
||||
|
||||
Before implementing any logic:
|
||||
|
||||
- [ ] **Pattern exists?** Search for similar patterns in the codebase first
|
||||
|
||||
```bash
|
||||
# Example: Before implementing debounced search
|
||||
rg "debounce" src/ packages/
|
||||
rg "useDebounce" src/ packages/
|
||||
```
|
||||
|
||||
- [ ] **Will repeat?** Will this exact logic be needed in 2+ places?
|
||||
- If yes -> Create a shared hook/utility **first**, then use it
|
||||
- Example: `useDebounce` instead of repeating debounce logic in 5 hooks
|
||||
|
||||
- [ ] **React Query pattern?** Is there an existing query/mutation hook for this data?
|
||||
- Search before creating: `rg "orpc.items" src/`
|
||||
- Check if you can extend an existing hook rather than creating a new one
|
||||
|
||||
- [ ] **Server or client?** Does this logic need interactivity?
|
||||
- If no -> Keep it in a Server Component (default)
|
||||
- If yes -> Extract only the interactive part into a Client Component
|
||||
|
||||
### 3. Types & Schemas
|
||||
|
||||
Before defining types:
|
||||
|
||||
- [ ] **Zod schema exists?** Is there already a Zod schema for this data shape?
|
||||
- Check the API module's `types.ts`: `rg "Schema = z.object" packages/api/`
|
||||
- Derive TypeScript types from Zod schemas with `z.infer<typeof schema>`
|
||||
- Never manually define a TypeScript interface that duplicates a Zod schema
|
||||
|
||||
- [ ] **Existing type?** Does a similar type already exist?
|
||||
- Search before creating: `rg "interface.*YourTypeName\|type.*YourTypeName" src/ packages/`
|
||||
|
||||
- [ ] **Cross-layer type?** Is this type used across the oRPC boundary?
|
||||
- If yes -> Define the Zod schema in the API module's `types.ts`, export the inferred type
|
||||
- Frontend should import types from the API package, not redefine them
|
||||
|
||||
- [ ] **Derived from client?** Can you infer the type from the oRPC client?
|
||||
```typescript
|
||||
// Prefer this over manually defining types
|
||||
type ItemResult = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>;
|
||||
```
|
||||
|
||||
### 4. UI Components
|
||||
|
||||
Before creating UI components:
|
||||
|
||||
- [ ] **Server or Client Component?** Does this component need:
|
||||
- Event handlers (onClick, onChange)? -> `'use client'`
|
||||
- React hooks (useState, useEffect)? -> `'use client'`
|
||||
- Browser APIs (window, document)? -> `'use client'`
|
||||
- None of the above? -> Keep as Server Component (default)
|
||||
|
||||
- [ ] **Similar component exists?** Search before creating
|
||||
- `rg "function.*YourComponent\|export.*YourComponent" src/`
|
||||
|
||||
- [ ] **Visual-logic consistency?** If there's already a visual distinction (icon, color, label) for a concept, does your logic match?
|
||||
|
||||
- [ ] **State lifecycle?** Will this component unmount during normal user flow?
|
||||
- If yes -> Consider where state should persist (URL params with nuqs, parent, context)
|
||||
|
||||
### 5. API Routes & oRPC Procedures
|
||||
|
||||
Before writing an API route or oRPC procedure:
|
||||
|
||||
- [ ] **Existing procedure?** Does a similar oRPC procedure already exist?
|
||||
- Check the module's `router.ts`: `rg "Router = {" packages/api/`
|
||||
- Can you extend an existing procedure rather than creating a new one?
|
||||
|
||||
- [ ] **Correct HTTP method?**
|
||||
- GET for read operations (queries)
|
||||
- POST for create operations (mutations)
|
||||
- PUT/PATCH for update operations
|
||||
- DELETE for removal operations
|
||||
|
||||
- [ ] **Authentication level?** Which base procedure to use?
|
||||
- Public data -> `publicProcedure`
|
||||
- User-specific data -> `protectedProcedure`
|
||||
- Admin operations -> `adminProcedure`
|
||||
|
||||
- [ ] **Input/output schemas defined?** Both should be Zod schemas in `types.ts`
|
||||
|
||||
### 6. Dependencies
|
||||
|
||||
Before adding a dependency:
|
||||
|
||||
- [ ] **Already installed?** Check `package.json` across all packages
|
||||
```bash
|
||||
rg "\"dependency-name\"" package.json packages/*/package.json
|
||||
```
|
||||
|
||||
- [ ] **Built-in alternative?** Can you use a native API or existing utility instead?
|
||||
- Example: `structuredClone()` instead of `lodash.cloneDeep`
|
||||
|
||||
- [ ] **Bundle impact?** Will this significantly increase the client bundle?
|
||||
- If yes -> Consider dynamic import or server-only usage
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Adding a value/constant?
|
||||
|-- Used in both app AND api package? -> shared package (@your-app/utils)
|
||||
|-- Used in 2+ files within same package? -> shared constants file
|
||||
+-- Single file only? -> Local constant is fine
|
||||
|
||||
Adding logic/behavior?
|
||||
|-- Similar pattern exists? -> Extend or reuse existing
|
||||
|-- Will be used in 2+ places? -> Create shared hook/utility first
|
||||
+-- Single use only? -> Implement directly (but document pattern)
|
||||
|
||||
Adding a type?
|
||||
|-- Zod schema exists? -> Use z.infer<typeof schema>
|
||||
|-- Crosses oRPC boundary? -> Define in API types.ts, import elsewhere
|
||||
|-- Can derive from client? -> Use Awaited<ReturnType<...>>
|
||||
+-- Local only? -> Define locally
|
||||
|
||||
Adding a component?
|
||||
|-- Needs interactivity? -> 'use client'
|
||||
|-- Pure display? -> Server Component (default)
|
||||
+-- Mix of both? -> Split into Server wrapper + Client interactive part
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What to Verify Across Layers
|
||||
|
||||
When implementing a feature that spans Server Component -> API -> Database, verify:
|
||||
|
||||
| Layer | Check |
|
||||
| ---------------- | ------------------------------------------------------------------ |
|
||||
| Server Component | Data fetched correctly? Props serializable? No client-only APIs? |
|
||||
| Client Component | Loading/error states handled? React Query cache invalidated? |
|
||||
| oRPC Procedure | Input validated? Auth checked? Output schema matches? |
|
||||
| Database Query | No N+1 queries? Proper indexes? Transactions where needed? |
|
||||
| Zod Schemas | Input and output schemas consistent? Date/null handling correct? |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Redefining Backend Types
|
||||
|
||||
```typescript
|
||||
// DON'T: Manually define types that mirror Zod schemas
|
||||
interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// DO: Import or infer from the source of truth
|
||||
import type { Item } from "@your-app/api/modules/items/types";
|
||||
// or
|
||||
type Item = Awaited<ReturnType<(typeof orpcClient)["items"]["get"]>>["item"];
|
||||
```
|
||||
|
||||
### Manual Query Keys
|
||||
|
||||
```typescript
|
||||
// DON'T: Manually construct query keys
|
||||
queryClient.invalidateQueries({ queryKey: ["items", "list"] });
|
||||
|
||||
// DO: Use oRPC generated keys
|
||||
queryClient.invalidateQueries({ queryKey: orpc.items.list.key() });
|
||||
```
|
||||
|
||||
### Unnecessary Client Components
|
||||
|
||||
```typescript
|
||||
// DON'T: Mark everything as 'use client'
|
||||
'use client';
|
||||
export function ItemCard({ item }) {
|
||||
return <div>{item.name}</div>; // No interactivity needed!
|
||||
}
|
||||
|
||||
// DO: Keep as Server Component when possible
|
||||
export function ItemCard({ item }) {
|
||||
return <div>{item.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch in Client Components When Server Would Work
|
||||
|
||||
```typescript
|
||||
// DON'T: Fetch in Client Component when data could come from Server Component
|
||||
'use client';
|
||||
export function ItemList() {
|
||||
const { data } = useQuery(orpc.items.list.queryOptions({ input: {} }));
|
||||
return <ul>{data?.items.map(...)}</ul>;
|
||||
}
|
||||
|
||||
// DO: Fetch in Server Component, pass as props (when no interactivity needed)
|
||||
export async function ItemList() {
|
||||
const data = await orpcClient.items.list({});
|
||||
return <ul>{data.items.map(...)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use This Checklist
|
||||
|
||||
| Trigger | Action |
|
||||
| ------------------------------------------ | ------------------------- |
|
||||
| About to add a constant | Run through Section 1 |
|
||||
| About to implement logic | Run through Section 2 |
|
||||
| About to define a type or schema | Run through Section 3 |
|
||||
| About to create a component | Run through Section 4 |
|
||||
| About to add an oRPC procedure | Run through Section 5 |
|
||||
| About to add a dependency | Run through Section 6 |
|
||||
| Feels like you've seen similar code before | **STOP** and search first |
|
||||
|
||||
---
|
||||
|
||||
## Relationship to Other Guides
|
||||
|
||||
| Guide | Focus | Timing |
|
||||
| ------------------------------------------------------------- | ------------------------- | --------------------------- |
|
||||
| **Pre-Implementation Checklist** (this) | Questions before coding | Before writing code |
|
||||
| [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md) | Data flow across layers | Complex feature planning |
|
||||
|
||||
**Ideal workflow:**
|
||||
|
||||
1. Read this checklist before coding
|
||||
2. Use Cross-Layer guide for features spanning multiple layers
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
| Date | Issue | Lesson |
|
||||
| ---- | ---------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| - | Zod schema redefined in frontend and backend | Always derive frontend types from the API package's Zod schemas |
|
||||
| - | `useQuery` used where Server Component sufficed | Ask "does this need interactivity?" before reaching for React Query |
|
||||
| - | Manual query keys diverged from oRPC keys | Always use `orpc.xxx.key()` or `orpc.xxx.queryKey()` for cache ops |
|
||||
| - | Type defined in both app and api package | Cross-boundary types must be defined once in the API module |
|
||||
|
||||
---
|
||||
|
||||
**Core Principle**: 5 minutes of checklist thinking saves 50 minutes of refactoring.
|
||||
Reference in New Issue
Block a user