11 KiB
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/utilsor@your-app/config) - Example:
MAX_UPLOAD_SIZEused by both file upload UI and oRPC validation
- If yes -> Put in a shared package (e.g.,
-
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 = 300in 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
# 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:
useDebounceinstead 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
- Search before creating:
-
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
- Check the API module's
-
Existing type? Does a similar type already exist?
- Search before creating:
rg "interface.*YourTypeName\|type.*YourTypeName" src/ packages/
- Search before creating:
-
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
- If yes -> Define the Zod schema in the API module's
-
Derived from client? Can you infer the type from the oRPC client?
// 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)
- Event handlers (onClick, onChange)? ->
-
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?
- Check the module's
-
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
- Public data ->
-
Input/output schemas defined? Both should be Zod schemas in
types.ts
6. Dependencies
Before adding a dependency:
-
Already installed? Check
package.jsonacross all packagesrg "\"dependency-name\"" package.json packages/*/package.json -
Built-in alternative? Can you use a native API or existing utility instead?
- Example:
structuredClone()instead oflodash.cloneDeep
- Example:
-
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
// 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
// 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
// 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
// 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 | Data flow across layers | Complex feature planning |
Ideal workflow:
- Read this checklist before coding
- 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.