--- name: code-review-wasp description: Comprehensive code review skill for Wasp applications. Use when reviewing code, PRs, or architectural changes. Covers Wasp-specific patterns, import rules, auth checks, TDD compliance, multi-tenant permissions, and performance. Integrates all CLAUDE.md critical rules. triggers: [ "review code", "code review", "review PR", "check code", "review this", "pull request review", "architectural review", "review changes", "check implementation", ] version: 1.0 last_updated: 2025-10-20 allowed_tools: [Read, Bash] --- # Code Review Wasp Skill ## Quick Reference **When to use this skill:** - Reviewing pull requests - Checking code before commit - Architectural review of features - Pre-merge quality checks - Identifying Wasp-specific issues **Key Focus Areas:** 1. **Wasp Framework Patterns** - Import rules, operations, entity access 2. **Security** - Server-side auth, multi-tenant isolation, input validation 3. **TDD Compliance** - 2-phase workflow, test quality (5 criteria) 4. **Performance** - N+1 queries, pagination, memoization 5. **Code Quality** - SOLID, DRY, testability **Review Philosophy:** This skill catches **Wasp-specific issues** that generic code review tools miss. It enforces the patterns documented in CLAUDE.md and your custom skills. --- ## Review Checklist (10 Critical Categories) Use this checklist systematically for every code review: ### ✅ Category 1: Import Rules **Verify correct import syntax based on context:** ```typescript // ✅ CORRECT - In .ts/.tsx files import { HttpError } from "wasp/server"; import type { Task } from "wasp/entities"; import { useQuery } from "wasp/client/operations"; import { TaskStatus } from "@prisma/client"; // For enum VALUES // ❌ WRONG - Common mistakes import { Task } from "@wasp/entities"; // Wrong prefix import { helper } from "@src/utils/helper"; // Wrong in .ts/.tsx import { TaskStatus } from "wasp/entities"; // Wrong for enum values ``` ```wasp // ✅ CORRECT - In main.wasp files query getTasks { fn: import { getTasks } from "@src/server/a3/operations", entities: [Task] } // ❌ WRONG - Common mistakes fn: import { getTasks } from "src/server/a3/operations", // Missing @ fn: import { getTasks } from "../src/server/a3/operations", // Relative path ``` **Red Flags:** - ❌ Using `@wasp/` prefix instead of `wasp/` - ❌ Using `@src/` in .ts/.tsx files (should be relative paths) - ❌ Using relative paths in main.wasp (should be `@src/`) - ❌ Importing enum values from `wasp/entities` (should be `@prisma/client`) **Cross-reference:** See `wasp-operations` skill for complete import patterns --- ### ✅ Category 2: Type Annotations (Operations) **CRITICAL: Type annotations are REQUIRED for context.entities access** ```typescript // ✅ CORRECT - Type annotation present export const getTasks: GetTasks = async (args, context) => { return context.entities.Task.findMany(); // Works! }; // ❌ WRONG - No type annotation export const getTasks = async (args, context) => { return context.entities.Task.findMany(); // context.entities is undefined! }; ``` **Check:** - [ ] All queries have `Get{Name}` type annotation - [ ] All actions have `Create/Update/Delete{Name}` type annotation - [ ] Imports include `from "wasp/server/operations"` - [ ] Args and Return types match actual function signature **Red Flags:** - ❌ Operations without type annotations - ❌ Type annotation doesn't match actual function signature - ❌ Missing imports for type definitions **Cross-reference:** See `wasp-operations` skill, lines 88-155 --- ### ✅ Category 3: Auth Checks (Security CRITICAL) **MANDATORY: Auth check must be FIRST line of every operation** ```typescript // ✅ CORRECT - Auth check first export const getTasks: GetTasks = async (args, context) => { if (!context.user) throw new HttpError(401); // FIRST line return context.entities.Task.findMany({ where: { userId: context.user.id } }); }; // ❌ WRONG - Auth check missing or late export const getTasks: GetTasks = async (args, context) => { const tasks = await context.entities.Task.findMany(); // NO AUTH CHECK! return tasks; }; ``` **Check:** - [ ] First line is: `if (!context.user) throw new HttpError(401)` - [ ] No operations bypass auth (unless explicitly public) - [ ] Auth check uses `context.user` (NOT custom logic) - [ ] Operations filter by `userId` or check ownership **Red Flags:** - ❌ Missing auth check entirely - ❌ Auth check after business logic - ❌ Custom auth logic instead of `context.user` - ❌ No filtering by userId (potential data leak) **Cross-reference:** See `wasp-operations` skill, Section 3 (lines 120-220) --- ### ✅ Category 4: Error Sequence (HTTP Status Codes) **CRITICAL: Enforce correct error sequence** **Order (MUST follow):** 1. **401 Unauthorized** - No user (unauthenticated) 2. **404 Not Found** - Resource doesn't exist 3. **403 Forbidden** - User lacks permission 4. **400 Bad Request** - Invalid input ```typescript // ✅ CORRECT - Proper error sequence export const updateTask: UpdateTask = async (args, context) => { // 1. Auth check (401) if (!context.user) throw new HttpError(401); // 2. Fetch resource const task = await context.entities.Task.findUnique({ where: { id: args.id }, }); // 3. Check existence (404) if (!task) throw new HttpError(404, "Task not found"); // 4. Check permission (403) if (task.userId !== context.user.id) { throw new HttpError(403, "Not authorized"); } // 5. Validate input (400) if (!args.data.description?.trim()) { throw new HttpError(400, "Description required"); } // 6. Perform update return context.entities.Task.update({ where: { id: args.id }, data: args.data, }); }; // ❌ WRONG - Wrong error order export const updateTask: UpdateTask = async (args, context) => { // Checking permission before auth and existence - WRONG ORDER! const task = await context.entities.Task.findUnique({ where: { id: args.id }, }); if (task.userId !== context.user.id) throw new HttpError(403); // ... rest of checks }; ``` **Check:** - [ ] 401 comes first (auth) - [ ] 404 before 403 (existence before permission) - [ ] 400 after auth/existence/permission checks - [ ] Error messages are descriptive **Red Flags:** - ❌ Wrong error order (e.g., 403 before 404) - ❌ Using wrong HTTP status code - ❌ Generic error messages ("Error", "Failed") - ❌ Not using HttpError class **Cross-reference:** See `error-handling` skill for complete patterns --- ### ✅ Category 5: Entity Listing (main.wasp) **CRITICAL: Entities must be listed for context.entities access AND auto-invalidation** ```wasp // ✅ CORRECT - Entities listed query getTasks { fn: import { getTasks } from "@src/server/a3/operations", entities: [Task] // Enables context.entities.Task + auto-invalidation } action createTask { fn: import { createTask } from "@src/server/a3/operations", entities: [Task] // Same as getTasks → auto-invalidation works! } // ❌ WRONG - No entities query getTasks { fn: import { getTasks } from "@src/server/a3/operations", // Missing entities: [] → context.entities undefined! } // ❌ WRONG - Different entities prevent auto-invalidation action createTask { entities: [Task, User] // Extra entity prevents auto-invalidation of getTasks } ``` **Check:** - [ ] All queries list entities in main.wasp - [ ] All actions list entities in main.wasp - [ ] Entities match actual database access - [ ] Query + Action share same entities (for auto-invalidation) **Red Flags:** - ❌ Missing `entities: []` array - ❌ Empty entities array when accessing database - ❌ Query and action have different entities (breaks auto-invalidation) - ❌ Entities listed but not actually accessed in code **Cross-reference:** See `wasp-operations` skill, lines 40-87 --- ### ✅ Category 6: Client Patterns (Actions) **DEFAULT: Use direct async/await for actions, NOT useAction** ```typescript // ✅ CORRECT - Direct call (default pattern) const handleCreate = async (description: string) => { try { await createTask({ description }); // Simple + enables auto-invalidation toast.success("Task created"); } catch (err) { toast.error(err.message); } }; // ❌ WRONG - useAction by default const createTaskFn = useAction(createTask); const handleCreate = async (description: string) => { try { await createTaskFn({ description }); // Blocks auto-invalidation! toast.success("Task created"); } catch (err) { toast.error(err.message); } }; ``` **ONLY use useAction for optimistic UI updates (advanced):** ```typescript // ✅ CORRECT - useAction ONLY for optimistic updates const deleteTaskFn = useAction(deleteTask, { optimisticUpdates: [ { getQuerySpecifier: () => [getTasks], updateQuery: (oldTasks, { id }) => oldTasks.filter((t) => t.id !== id), }, ], }); ``` **Check:** - [ ] Actions use direct `await action(args)` by default - [ ] `useAction` ONLY used with `optimisticUpdates` config - [ ] Client imports from `wasp/client/operations` - [ ] Error handling present (try/catch) **Red Flags:** - ❌ Using `useAction` without optimistic updates - ❌ Importing operations from wrong location - ❌ No error handling around action calls - ❌ Manual cache invalidation (Wasp does this automatically) **Cross-reference:** See CLAUDE.md, lines 275-304 (Client-Side Action Usage) --- ### ✅ Category 7: Restart Requirements **MANDATORY: Restart after schema.prisma or main.wasp changes** **Files that require restart:** - `schema.prisma` - Database schema changes - `main.wasp` - Routes, operations, entities changes - Database migrations - After `wasp db migrate-dev` **Verify:** ```bash # After changes, MUST restart with safe-start.sh (multi-worktree safe) ../scripts/safe-start.sh # ❌ WRONG - Direct wasp start (not multi-worktree safe) wasp start ``` **Check:** - [ ] PR mentions restart if schema/main.wasp changed - [ ] Uses `../scripts/safe-start.sh` (not direct `wasp start`) - [ ] Migration ran BEFORE restart (if schema changed) - [ ] Types verified after restart **Red Flags:** - ❌ Schema/main.wasp changes without restart mention - ❌ Using direct `wasp start` (multi-worktree conflicts) - ❌ Restart before migration (wrong order) - ❌ Assuming types update without restart **Cross-reference:** See `wasp-database` skill for complete workflow --- ### ✅ Category 8: Test Coverage (TDD Compliance) **MANDATORY: ≥80% statements, ≥75% branches** **Check test quality using 5 MUST PASS criteria:** **1. Tests Business Logic (NOT Existence):** ```typescript // ❌ WRONG - Test theater it("should exist", () => { expect(createTask).toBeDefined(); // Meaningless! }); // ✅ CORRECT - Tests behavior it("should create task with description", async () => { const result = await createTask({ description: "Test" }, mockContext); expect(result.description).toBe("Test"); expect(mockContext.entities.Task.create).toHaveBeenCalledWith({ data: { description: "Test", userId: "user-1" }, }); }); ``` **2. Meaningful Assertions:** - ❌ `expect(result).toBeDefined()` - Generic - ✅ `expect(result.name).toBe('Acme')` - Specific **3. Tests Error Paths:** - ✅ 401 (unauthenticated) - ✅ 400 (validation error) - ✅ 404 (not found) - ✅ 403 (no permission) **4. Tests Edge Cases:** - ✅ Empty strings, null, undefined - ✅ Boundary conditions (min/max length) - ✅ Special characters **5. Behavior NOT Implementation:** - ❌ Testing internal variables (`component.state.loading`) - ✅ Testing observable UI (`screen.getByText('Loaded')`) **Check:** - [ ] All 5 quality criteria pass - [ ] Coverage ≥80% statements, ≥75% branches - [ ] Tests follow 2-phase TDD (RED → GREEN + REFACTOR) - [ ] No test theater (existence checks) **Red Flags:** - ❌ Low coverage (<80%/<75%) - ❌ Only happy path tests (no error paths) - ❌ Existence checks instead of behavior tests - ❌ Testing implementation details **Cross-reference:** See `tdd-workflow` skill, lines 226-555 --- ### ✅ Category 9: ShadCN Version Lock **CRITICAL: ONLY use ShadCN v2.3.0** ```bash # ✅ CORRECT - Version locked npx shadcn@2.3.0 add button # ❌ WRONG - Uses latest (Tailwind v4, breaks Wasp) npx shadcn add button ``` **After EVERY component installation - Fix import path:** ```diff // In src/components/ui/{component}.tsx -import { cn } from "s/lib/utils" +import { cn } from "../../lib/utils" ``` **Check:** - [ ] Uses `shadcn@2.3.0` (NOT latest) - [ ] Import path fixed to `../../lib/utils` - [ ] No Tailwind v4 syntax - [ ] Component works without import errors **Red Flags:** - ❌ Using `shadcn` without version (uses latest) - ❌ Import path still `s/lib/utils` - ❌ Tailwind v4 syntax (@theme, etc) - ❌ Component import errors **Cross-reference:** See `shadcn-ui` skill and CLAUDE.md lines 82-107 --- ### ✅ Category 10: Multi-Tenant Permissions **CRITICAL: Verify multi-tenant isolation** **Architecture:** - Organization → Many Departments (hierarchical via `parentId`) - Users ↔ Departments = Many-to-Many via `UserDepartment` - DepartmentRole: MANAGER | MEMBER | VIEWER **Permission checks REQUIRED:** ```typescript // ✅ CORRECT - Multi-tenant filtering export const getTasks: GetTasks = async (args, context) => { if (!context.user) throw new HttpError(401); // Get user's departments const userDepartments = await context.entities.UserDepartment.findMany({ where: { userId: context.user.id }, }); const departmentIds = userDepartments.map((ud) => ud.departmentId); // Filter by departments user has access to return context.entities.Task.findMany({ where: { departmentId: { in: departmentIds }, }, }); }; // ❌ WRONG - No multi-tenant filtering export const getTasks: GetTasks = async (args, context) => { if (!context.user) throw new HttpError(401); return context.entities.Task.findMany(); // Returns ALL tasks (data leak!) }; ``` **Check:** - [ ] Operations filter by organization/department - [ ] Permission checks use DepartmentRole - [ ] No raw queries without multi-tenant WHERE clause - [ ] VIEWER role has read-only access **Red Flags:** - ❌ Queries without organization/department filtering - ❌ Assuming single organization - ❌ No role-based permission checks - ❌ Data leaks across organizations **Cross-reference:** See `permissions` skill for complete patterns --- ## Wasp-Specific Red Flags These issues are UNIQUE to Wasp - generic tools won't catch them: ### 🚩 Critical Red Flags (BLOCK PR) 1. **Missing type annotations on operations** - Symptom: `context.entities` is undefined at runtime - Impact: Runtime crash - Fix: Add `GetQuery` type annotation 2. **Auth check not first line** - Symptom: Security vulnerability - Impact: Unauthorized access to data - Fix: Move `if (!context.user)` to first line 3. **Using `@wasp/` prefix** - Symptom: Cannot find module error - Impact: Build fails - Fix: Use `wasp/` (no @) 4. **ShadCN version > 2.3.0** - Symptom: Tailwind v4 incompatibility - Impact: Styles break, build fails - Fix: Uninstall and reinstall with `@2.3.0` 5. **No restart after schema/main.wasp changes** - Symptom: Types are stale, imports fail - Impact: Development blocked - Fix: Stop wasp, run `../scripts/safe-start.sh` 6. **Test theater (5 criteria fail)** - Symptom: Tests pass but don't verify behavior - Impact: False confidence, bugs in production - Fix: Rewrite tests following 5 MUST PASS criteria ### ⚠️ Warning Red Flags (Request Changes) 7. **Using `useAction` by default** - Impact: Blocks auto-invalidation - Fix: Use direct `await action(args)` 8. **Different entities in query + action** - Impact: Manual cache invalidation needed - Fix: Use same entities for auto-invalidation 9. **N+1 query problem** - Impact: Performance degradation - Fix: Use Prisma `include` for relations 10. **No multi-tenant filtering** - Impact: Data leaks across organizations - Fix: Filter by organization/department in WHERE clause 11. **Wrong error sequence** - Impact: Misleading error messages - Fix: Follow 401 → 404 → 403 → 400 order 12. **Direct `wasp start` instead of `safe-start.sh`** - Impact: Multi-worktree port conflicts - Fix: Use `../scripts/safe-start.sh` --- ## Code Quality Criteria ### SOLID Principles **Single Responsibility:** - ✅ Operations do ONE thing (get, create, update, delete) - ❌ Operations mixing CRUD operations - ❌ Operations handling multiple entities **Open/Closed:** - ✅ Helper functions for reusable logic - ❌ Copy-pasted auth checks - ❌ Copy-pasted validation logic **Liskov Substitution:** - ✅ Consistent return types - ❌ Sometimes returns entity, sometimes null - ❌ Inconsistent error handling **Interface Segregation:** - ✅ Minimal operation arguments - ❌ Operations accepting huge config objects - ❌ Optional parameters that are actually required **Dependency Inversion:** - ✅ Using Wasp operations abstraction - ❌ Direct Prisma imports in client code - ❌ Tight coupling to specific implementations ### DRY (Don't Repeat Yourself) **Extract common patterns:** ```typescript // ✅ CORRECT - Extracted helper function requireAuth(context: any) { if (!context.user) throw new HttpError(401); return context.user; } export const getTasks: GetTasks = async (args, context) => { requireAuth(context); // Reused return context.entities.Task.findMany(); }; // ❌ WRONG - Repeated auth check export const getTasks: GetTasks = async (args, context) => { if (!context.user) throw new HttpError(401); // Repeated // ... }; export const getTask: GetTask = async (args, context) => { if (!context.user) throw new HttpError(401); // Repeated // ... }; ``` **Check:** - [ ] Auth checks extracted to helper - [ ] Validation logic reused - [ ] Permission checks centralized - [ ] No copy-pasted code ### Testability **Code should be easy to test:** - ✅ Pure functions (input → output, no side effects) - ✅ Dependencies injected (via context) - ✅ Minimal mocking required - ❌ Global state access - ❌ Hard-coded values - ❌ Untestable private logic --- ## Security Review ### Server-Side Authorization **CRITICAL: Never trust client** ```typescript // ❌ WRONG - Client-side check only if (user.role !== "ADMIN") return
Access denied
; // Easy to bypass! Client-side is cosmetic only. // ✅ CORRECT - Server-side enforcement export const sensitiveOperation: Operation = async (args, context) => { if (!context.user) throw new HttpError(401); if (context.user.role !== "ADMIN") throw new HttpError(403); // Safe - enforced server-side }; ``` **Check:** - [ ] All permissions checked server-side - [ ] Operations throw 403 if unauthorized - [ ] Client checks are cosmetic only (UX) - [ ] No sensitive data leaked to client ### Input Validation **Validate ALL user input:** ```typescript // ✅ CORRECT - Validation if (!args.description?.trim()) { throw new HttpError(400, "Description required"); } if (args.description.length > 500) { throw new HttpError(400, "Description too long"); } // ❌ WRONG - No validation const task = await context.entities.Task.create({ data: { description: args.description }, // Accepts empty string, null, etc. }); ``` **Check:** - [ ] Required fields validated - [ ] Length constraints enforced - [ ] Format validation (email, URL, etc.) - [ ] Special characters handled ### SQL Injection Prevention **Use Prisma (auto-escapes):** ```typescript // ✅ CORRECT - Prisma auto-escapes const tasks = await context.entities.Task.findMany({ where: { status: args.status }, // Safe }); // ❌ WRONG - Raw SQL (dangerous!) const tasks = await context.entities.$queryRaw` SELECT * FROM Task WHERE status = ${args.status} `; // SQL injection risk! ``` **Check:** - [ ] All queries use Prisma (not raw SQL) - [ ] No string concatenation in queries - [ ] User input never directly in SQL --- ## Performance Review ### N+1 Query Problem **CRITICAL: Avoid multiple queries in loop** ```typescript // ❌ WRONG - N+1 queries const tasks = await context.entities.Task.findMany(); for (const task of tasks) { const user = await context.entities.User.findUnique({ where: { id: task.userId }, }); // N queries! } // ✅ CORRECT - Single query with include const tasks = await context.entities.Task.findMany({ include: { user: true }, // 1 query with JOIN }); ``` **Check:** - [ ] No queries inside loops - [ ] Related data fetched with `include` - [ ] Uses Prisma select for specific fields ### Pagination **Required for large lists:** ```typescript // ❌ WRONG - Loading all records const tasks = await context.entities.Task.findMany(); // Could be millions! // ✅ CORRECT - Paginated const tasks = await context.entities.Task.findMany({ skip: page * pageSize, take: pageSize, orderBy: { createdAt: "desc" }, }); ``` **Check:** - [ ] Lists use pagination (skip/take) - [ ] Default page size reasonable (20-50) - [ ] Returns total count for UI ### React Re-renders **Memoization for expensive components:** ```typescript // ✅ CORRECT - Memoized const ExpensiveComponent = memo(({ data }) => { const processed = useMemo(() => expensiveCalc(data), [data]); return
{processed}
; }); // ❌ WRONG - Re-renders every time function ExpensiveComponent({ data }) { const processed = expensiveCalc(data); // Runs on every render! return
{processed}
; } ``` **Check:** - [ ] Expensive components use `memo` - [ ] Expensive calculations use `useMemo` - [ ] Stable callbacks use `useCallback` --- ## PR Review Template Use this template for structured feedback: ````markdown ## Code Review Summary **Overall:** [APPROVE / REQUEST CHANGES / BLOCK] **Wasp-Specific Issues:** [count] found **Security Issues:** [count] found **Performance Issues:** [count] found **Test Quality:** [PASS / FAIL - specify criteria] --- ## 🚨 Critical Issues (MUST FIX) ### Issue 1: [Title] **Location:** `app/src/server/a3/operations.ts:45` **Problem:** Missing type annotation on operation. `context.entities` will be undefined at runtime. **Current Code:** ```typescript export const getTasks = async (args, context) => { return context.entities.Task.findMany(); // FAILS }; ``` ```` **Fix:** ```typescript export const getTasks: GetTasks = async (args, context) => { return context.entities.Task.findMany(); // Works }; ``` **Why:** Type annotations are CRITICAL for Wasp operations to enable `context.entities` access. **Reference:** `wasp-operations` skill, lines 88-155 --- ## ⚠️ Warnings (Request Changes) ### Issue 2: [Title] [Same structure as Critical Issues] --- ## ✅ What Went Well - Auth checks present and correct - Test coverage excellent (87%/82%) - Code follows DRY principle - Multi-tenant filtering correct --- ## 📚 Additional Resources - [CLAUDE.md](../../CLAUDE.md) - Project guidelines - [wasp-operations skill](../wasp-operations/SKILL.md) - Operation patterns - [tdd-workflow skill](../tdd-workflow/SKILL.md) - Test quality criteria - [Troubleshooting Guide](../../docs/TROUBLESHOOTING-GUIDE.md) - Common issues --- ## 🎯 Action Items - [ ] Fix all CRITICAL issues (block PR) - [ ] Address all WARNING issues - [ ] Verify test coverage ≥80%/≥75% - [ ] Run `../scripts/safe-start.sh` after schema/main.wasp changes - [ ] Update PR description with testing notes ``` --- ## Summary **This skill provides comprehensive Wasp-specific code review.** **Key Features:** - ✅ Catches Wasp-specific issues (generic tools miss these) - ✅ Enforces all CLAUDE.md critical rules - ✅ Integrates with existing skills (cross-references) - ✅ Provides actionable feedback (not just "good/bad") - ✅ Context-efficient (activates only when explicitly asked) **When to activate:** - Reviewing PRs before merge - Pre-commit quality checks - Architectural reviews - Identifying framework-specific issues **Expected outcome:** Structured review with: 1. Critical issues (block PR) 2. Warnings (request changes) 3. What went well (positive feedback) 4. Actionable fixes (code examples) 5. Cross-references (related skills) **For complete patterns:** See cross-referenced skills listed throughout this document. ```