pinpoint-patterns
Project-specific code patterns for Server Actions, data fetching, error handling, file organization. Use when implementing features to follow established patterns, or when user asks about project conventions/patterns/architecture.
When & Why to Use This Skill
This Claude skill serves as a comprehensive architectural guide and pattern library for modern web development, specifically optimized for Next.js, Drizzle ORM, and Supabase stacks. It streamlines the development lifecycle by providing standardized templates for Server Actions, efficient data fetching strategies using React 19 caching, and robust error-handling protocols. By enforcing consistent file organization and naming conventions, it helps engineering teams maintain high code quality, reduce technical debt, and ensure seamless project scalability.
Use Cases
- Feature Implementation: Use as a reference when building new modules to ensure Server Actions follow the mandatory 'auth-validate-action-revalidate' workflow.
- Code Standardization: Applying consistent naming conventions and directory structures (e.g., separating data-access layers from server actions) to improve codebase maintainability.
- Performance Optimization: Implementing standardized data fetching patterns using React 19's cache() to deduplicate database queries across server components.
- Onboarding & Documentation: Serving as a 'living' style guide for new developers to quickly learn project-specific patterns for error handling, type safety, and testing.
| name | pinpoint-patterns |
|---|---|
| description | Project-specific code patterns for Server Actions, data fetching, error handling, file organization. Use when implementing features to follow established patterns, or when user asks about project conventions/patterns/architecture. |
PinPoint Patterns Guide
When to Use This Skill
Use this skill when:
- Implementing new features
- Wondering "how should I structure this?"
- Looking for established patterns to follow
- Contributing new patterns after implementing same approach 2+ times
- User mentions: "pattern", "convention", "architecture", "how to", "best practice"
Quick Reference
Pattern Discovery Process
- Check
docs/PATTERNS.md(index of all patterns) - Look in
docs/patterns/for specific pattern files - If implementing same approach 2+ times, add new pattern
- Focus on PinPoint-specific patterns (not general Next.js knowledge)
Core Patterns
- Server Actions: File organization, auth, validation, revalidation
- Data Fetching: Drizzle queries, caching, error handling
- Error Handling: Validation errors, auth errors, DB errors
- File Organization: Where to put files, naming conventions
Detailed Documentation
Read these files for all established patterns:
# Index of all patterns
cat docs/PATTERNS.md
# Specific pattern files
ls docs/patterns/
cat docs/patterns/*.md
# Example: Data fetching patterns
cat docs/patterns/data-fetching.md
Server Action Patterns
File Organization
src/server/actions/
├── issues.ts # Issue-related actions
├── machines.ts # Machine-related actions
├── users.ts # User-related actions
└── comments.ts # Comment-related actions
Server Action Template
"use server";
import { createClient } from "~/lib/supabase/server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db } from "~/server/db";
import { issues } from "~/server/db/schema";
// 1. Define validation schema
const createIssueSchema = z.object({
title: z.string().min(1, "Title required"),
description: z.string().optional(),
machineId: z.string().uuid(),
severity: z.enum(["minor", "playable", "unplayable"]),
});
// 2. Define return type
export type CreateIssueResult =
| { success: true; issueId: string }
| { success: false; error: string };
// 3. Implement action
export async function createIssue(
formData: FormData
): Promise<CreateIssueResult> {
// 3a. Auth check (ALWAYS first)
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
// 3b. Validate input
const rawData = {
title: formData.get("title"),
description: formData.get("description"),
machineId: formData.get("machineId"),
severity: formData.get("severity"),
};
let validData;
try {
validData = createIssueSchema.parse(rawData);
} catch (error) {
return { success: false, error: "Invalid input" };
}
// 3c. Perform action
try {
const [issue] = await db
.insert(issues)
.values({
...validData,
reportedBy: user.id,
})
.returning();
// 3d. Revalidate affected paths
revalidatePath("/issues");
revalidatePath(`/machines/${validData.machineId}`);
return { success: true, issueId: issue.id };
} catch (error) {
return { success: false, error: "Failed to create issue" };
}
}
Data Fetching Patterns
Direct Drizzle Queries
// src/server/data-access/issues.ts
import { db } from "~/server/db";
import { issues, machines } from "~/server/db/schema";
import { eq, desc } from "drizzle-orm";
import { cache } from "react";
// ✅ Good: Cached data access function
export const getIssuesForMachine = cache(async (machineId: string) => {
if (!machineId) {
throw new Error("Machine ID required");
}
return await db.query.issues.findMany({
where: eq(issues.machineId, machineId),
orderBy: desc(issues.createdAt),
with: {
machine: true,
reportedByUser: {
columns: {
id: true,
email: true,
fullName: true,
},
},
},
});
});
// Usage in Server Component
export default async function MachineIssuesPage({
params,
}: {
params: { machineId: string };
}) {
const issues = await getIssuesForMachine(params.machineId);
return (
<div>
{issues.map((issue) => (
<IssueCard key={issue.id} issue={issue} />
))}
</div>
);
}
Caching Pattern
import { cache } from "react";
// ✅ Good: Wrap data access in cache()
export const getMachines = cache(async () => {
return await db.query.machines.findMany({
orderBy: (machines, { asc }) => [asc(machines.name)],
});
});
// React 19 cache() deduplicates calls within same request
// Multiple components can call getMachines() without extra DB queries
Error Handling Patterns
Validation Errors
// ✅ Good: Return structured errors
export type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function createMachine(
formData: FormData
): Promise<ActionResult<Machine>> {
const schema = z.object({
name: z.string().min(1),
manufacturer: z.string().min(1),
});
const result = schema.safeParse({
name: formData.get("name"),
manufacturer: formData.get("manufacturer"),
});
if (!result.success) {
return {
success: false,
error: result.error.issues[0]?.message ?? "Validation failed",
};
}
// Proceed with valid data
const [machine] = await db.insert(machines).values(result.data).returning();
return { success: true, data: machine };
}
Database Errors
export async function deleteIssue(
issueId: string
): Promise<ActionResult<void>> {
try {
await db.delete(issues).where(eq(issues.id, issueId));
revalidatePath("/issues");
return { success: true, data: undefined };
} catch (error) {
console.error("Failed to delete issue:", error);
return { success: false, error: "Failed to delete issue" };
}
}
File Organization Patterns
Directory Structure
src/
├── app/ # Next.js App Router
│ ├── (app)/ # Authenticated app routes
│ │ ├── machines/
│ │ ├── issues/
│ │ └── layout.tsx
│ ├── (auth)/ # Authentication routes
│ │ ├── login/
│ │ └── signup/
│ └── api/ # API routes (minimal, prefer Server Actions)
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── issues/ # Issue-specific components
│ ├── machines/ # Machine-specific components
│ └── layout/ # Layout components (nav, header, etc.)
├── lib/
│ ├── utils.ts # Utility functions
│ ├── types.ts # Shared types
│ ├── validation/ # Zod schemas
│ └── supabase/
│ ├── client.ts # Client-side Supabase
│ └── server.ts # Server-side Supabase
├── server/
│ ├── actions/ # Server Actions
│ ├── data-access/ # Data fetching functions
│ └── db/
│ ├── index.ts # Drizzle client
│ └── schema.ts # Drizzle schema
└── test/
├── unit/ # Unit tests
└── integration/
└── supabase/ # Integration tests
Naming Conventions
- Server Actions:
createIssue,updateMachine,deleteComment(verb + noun) - Data Access:
getIssuesForMachine,getMachineById(get + descriptive) - Components:
IssueCard,MachineList,CreateIssueDialog(PascalCase) - Utilities:
formatDate,calculateSeverity(camelCase) - Types:
Issue,Machine,User(PascalCase, singular)
Type Patterns
Database vs Application Types
// Database types (snake_case) - from schema
import { issues } from "~/server/db/schema";
type DbIssue = typeof issues.$inferSelect;
// Application types (camelCase) - in ~/lib/types
export type Issue = {
id: string;
title: string;
description: string | null;
machineId: string;
severity: "minor" | "playable" | "unplayable";
createdAt: Date;
};
// Converter at boundary
export function dbIssueToIssue(dbIssue: DbIssue): Issue {
return {
id: dbIssue.id,
title: dbIssue.title,
description: dbIssue.description,
machineId: dbIssue.machine_id,
severity: dbIssue.severity,
createdAt: new Date(dbIssue.created_at),
};
}
Revalidation Patterns
import { revalidatePath, revalidateTag } from "next/cache";
// ✅ Good: Revalidate specific paths
export async function createIssue(formData: FormData) {
// ... create issue ...
revalidatePath("/issues"); // List page
revalidatePath(`/machines/${machineId}`); // Machine detail page
revalidatePath(`/issues/${issueId}`); // Issue detail page
}
// ✅ Good: Revalidate by tag (for complex dependencies)
export async function updateMachine(machineId: string, data: MachineData) {
// ... update machine ...
revalidateTag(`machine-${machineId}`); // All queries tagged with this
}
Testing Patterns
Unit Test Pattern
// src/test/unit/lib/utils.test.ts
import { describe, it, expect } from "vitest";
import { calculateSeverityScore } from "~/lib/utils";
describe("calculateSeverityScore", () => {
it("returns 10 for unplayable", () => {
expect(calculateSeverityScore("unplayable")).toBe(10);
});
it("returns 5 for playable", () => {
expect(calculateSeverityScore("playable")).toBe(5);
});
});
Integration Test Pattern
// src/test/integration/supabase/issues.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { getPGlite } from "~/test/setup/pglite";
import { getIssuesForMachine } from "~/server/data-access/issues";
describe("getIssuesForMachine", () => {
beforeAll(async () => {
const db = getPGlite();
// Seed test data
await db.exec("INSERT INTO machines ...");
await db.exec("INSERT INTO issues ...");
});
it("returns issues for machine", async () => {
const issues = await getIssuesForMachine("machine-id");
expect(issues).toHaveLength(2);
});
});
Contributing New Patterns
When you implement the same approach 2+ times:
- Identify the pattern: What problem does it solve?
- Document it: Add to
docs/patterns/or updatedocs/PATTERNS.md - Include example: Show concrete code, not just description
- Keep it concise: Focus on PinPoint-specific patterns
Example contribution:
## New Pattern: Multi-Step Forms
**When to use**: Forms with multiple steps (e.g., machine setup wizard)
**Pattern**:
1. Use React 19 `useActionState` for state management
2. Store step number in state
3. Conditionally render step components
4. Final step calls Server Action
**Example**: See `src/components/machines/CreateMachineWizard.tsx`
Pattern Checklist
Before implementing a feature:
- Checked
docs/PATTERNS.mdfor existing patterns - Reviewed similar features in codebase
- Following Server Action template (auth → validate → action → revalidate)
- Using React 19
cache()for data fetching - Returning structured results from Server Actions
- Adding new pattern if implementing 2+ times
Additional Resources
- Pattern index:
docs/PATTERNS.md - Specific patterns:
docs/patterns/*.md - Non-negotiables:
docs/NON_NEGOTIABLES.md - Architecture:
docs/TECH_SPEC.md