
architect
by nickstrad
SKILL.md
name: architect description: Provides architectural guidance and design patterns for implementing new features in the Nick Stack codebase. Use when planning implementations, designing new features, or making architectural decisions.
You are an expert software architect specializing in modern full-stack TypeScript applications. Your role is to guide the design and implementation of new features in this Nick Stack codebase, ensuring consistency with established patterns and architectural principles.
Tech Stack Architecture
Core Technologies
- Framework: Next.js 16.1 (App Router) - React meta-framework with server components
- Language: TypeScript - Type-safe throughout the stack
- Database: PostgreSQL with Prisma ORM - Type-safe database access
- API Layer: tRPC - End-to-end type safety between frontend and backend
- Authentication: Better Auth - Modern auth with email + OAuth providers
- UI Framework: shadcn/ui + Radix UI - Accessible component library
- Styling: Tailwind CSS 4 - Utility-first CSS framework
- State Management: TanStack React Query - Server state management
- Form Handling: React Hook Form + Zod - Type-safe form validation
Architectural Principles
- Type Safety Everywhere: Leverage TypeScript, Zod, and tRPC for compile-time and runtime type safety
- Server-First Architecture: Prefer server components, only use client components when needed
- Feature-Based Organization: Group related code by feature, not by technical layer
- Separation of Concerns: Keep server logic separate from client logic
- Progressive Enhancement: Build for server rendering first, add client interactivity as needed
- Convention Over Configuration: Follow established patterns consistently
Project Structure & File Organization
Directory Structure
src/
├── app/ # Next.js App Router (pages & layouts)
│ ├── (auth)/ # Route group for auth pages
│ │ ├── login/page.tsx
│ │ └── signup/page.tsx
│ ├── api/ # API route handlers
│ │ ├── auth/[...all]/route.ts # Better Auth handler
│ │ └── trpc/[trpc]/route.ts # tRPC handler
│ ├── [feature]/ # Feature pages
│ │ ├── page.tsx # List view (server component)
│ │ ├── [id]/page.tsx # Detail view (server component)
│ │ └── create/page.tsx # Create view (server component)
│ └── layout.tsx # Root layout
│
├── components/
│ ├── app/ # App-specific shared components
│ │ └── AppLogo.tsx
│ └── ui/ # shadcn/ui components (don't modify)
│ ├── button.tsx
│ ├── form.tsx
│ └── ...
│
├── features/ # Feature modules (main organization)
│ └── [feature-name]/
│ ├── components/ # Feature-specific components
│ │ ├── [feature]-form.tsx
│ │ ├── [feature]-list.tsx
│ │ ├── [feature]-list-loading.tsx
│ │ ├── [feature]-list-error.tsx
│ │ └── [feature]-editor.tsx
│ ├── hooks/ # Feature-specific hooks
│ │ └── use-[feature].ts
│ ├── server/ # Server-side feature code
│ │ ├── router.ts # tRPC router for this feature
│ │ ├── params.ts # URL params parser/loader
│ │ └── prefetch.ts # Server-side data prefetching
│ ├── constants.ts # Feature constants
│ └── types.ts # Feature-specific types
│
├── lib/ # Shared utilities and configuration
│ ├── auth.ts # Better Auth configuration
│ ├── auth-client.ts # Better Auth client setup
│ ├── auth-utils.ts # Auth helper functions
│ ├── db.ts # Prisma client singleton
│ ├── constants.ts # Global constants
│ └── utils/ # Utility functions
│ └── css-helpers.ts
│
├── trpc/ # tRPC configuration
│ ├── routers/
│ │ └── _app.ts # Root router (combines all feature routers)
│ ├── client.tsx # Client-side tRPC setup
│ ├── server.tsx # Server-side tRPC setup
│ ├── init.ts # tRPC initialization and procedures
│ └── query-client.ts # React Query client config
│
└── generated/ # Generated code (don't modify manually)
└── prisma/ # Prisma generated types
Feature Module Pattern
When creating a new feature, follow this structure:
features/[feature-name]/
├── components/ # UI components
├── hooks/ # React hooks for data fetching
├── server/ # Server-side logic
│ ├── router.ts # tRPC API endpoints
│ ├── params.ts # URL parameter handling
│ └── prefetch.ts # Data prefetching
└── types.ts # Shared types
Critical Implementation Patterns
⚠️ Server-Side Prefetch Pattern (REQUIRED)
ALWAYS use this pattern for prefetch functions:
// src/features/[feature]/server/prefetch.ts
import type { inferInput } from "@trpc/tanstack-react-query";
import { prefetch, trpc } from "@/trpc/server";
// Use inferInput to get type-safe input from tRPC procedure
type PrefetchItemsInput = inferInput<typeof trpc.feature.getMany>;
// MUST be async function
// MUST use prefetch() helper from @/trpc/server
// MUST use .queryOptions() method
export async function prefetchItems(input: PrefetchItemsInput) {
return prefetch(trpc.feature.getMany.queryOptions(input));
}
❌ DON'T do this (old pattern):
// ❌ Missing inferInput, missing async, using wrong api import
export const prefetchItems = (params: Params) => {
void api.feature.getMany.prefetch(params);
};
⚠️ Client Hooks Pattern (REQUIRED)
ALWAYS use this pattern for client-side tRPC hooks:
// src/features/[feature]/hooks/use-items.ts
"use client";
import { useTRPC } from "@/trpc/client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
// ✅ Define types inline - DON'T import server-side param types
type ItemsParams = {
page: number;
pageSize: number;
};
export function useItems(params: ItemsParams) {
const trpc = useTRPC(); // ✅ Use useTRPC() hook
return useQuery(trpc.feature.getMany.queryOptions(params)); // ✅ Use useQuery with .queryOptions()
}
export function useCreateItem() {
const trpc = useTRPC(); // ✅ Get trpc instance
const queryClient = useQueryClient(); // ✅ Get queryClient for invalidation
return useMutation( // ✅ Use useMutation directly
trpc.feature.create.mutationOptions({ // ✅ Use .mutationOptions()
onSuccess: (data) => {
toast.success("Item created");
// ✅ Invalidate using queryClient and .queryOptions()
queryClient.invalidateQueries(
trpc.feature.getMany.queryOptions({ page: 1, pageSize: 20 })
);
},
onError: (error) => {
toast.error(error.message);
},
})
);
}
❌ DON'T do this (old pattern):
"use client";
import { trpc } from "@/trpc/client"; // ❌ Wrong import
import type { Params } from "../server/params"; // ❌ Don't import server types
export function useItems(params: Params) { // ❌ Server type in client
return trpc.feature.getMany.useQuery(params); // ❌ Direct .useQuery()
}
export function useCreateItem() {
const utils = trpc.useUtils(); // ❌ Old pattern
return trpc.feature.create.useMutation({ // ❌ Direct .useMutation()
onSuccess: () => {
utils.feature.getMany.invalidate(); // ❌ Old invalidation pattern
},
});
}
Key Rules
- Prefetch files: Use
inferInput, async functions,prefetch()helper, and.queryOptions() - Client hooks: Use
useTRPC(), explicituseQuery/useMutation,queryClient.invalidateQueries() - Type separation: NEVER import server-side param types (from
server/params.ts) into client hooks - Toast notifications: Include user feedback with
toast.success()andtoast.error()
Design Patterns for Common Scenarios
1. Adding a New CRUD Feature
Architecture Pattern:
User Request (Page)
↓ Server Component
├─→ Load search params (server)
├─→ Prefetch data (server)
└─→ HydrateClient
└─→ Client Component
└─→ tRPC hooks
└─→ React Query cache
Implementation Steps:
A. Define Prisma Schema
// prisma/schema.prisma
model Feature {
id String @id @default(cuid())
name String
description String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("feature")
}
B. Create tRPC Router
// src/features/feature-name/server/router.ts
import { createTRPCRouter, baseProcedure } from "@/trpc/init";
import prisma from "@/lib/db";
import { z } from "zod";
import { PAGINATION } from "@/lib/constants";
// Input validation schema
const featureInputSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
});
export const featureRouter = createTRPCRouter({
// List with pagination
getMany: baseProcedure
.input(
z.object({
page: z.number().default(PAGINATION.DEFAULT_PAGE),
pageSize: z.number()
.min(PAGINATION.MIN_PAGE_SIZE)
.max(PAGINATION.MAX_PAGE_SIZE)
.default(PAGINATION.DEFAULT_PAGE_SIZE),
search: z.string().default(""),
})
)
.query(async ({ input, ctx }) => {
const { page, pageSize, search } = input;
const skip = (page - 1) * pageSize;
const where = {
userId: ctx.user.id, // Filter by current user
...(search && {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ description: { contains: search, mode: "insensitive" as const } },
],
}),
};
const [items, total] = await Promise.all([
prisma.feature.findMany({
where,
skip,
take: pageSize,
orderBy: { createdAt: "desc" },
}),
prisma.feature.count({ where }),
]);
return {
items,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
}),
// Get single item
getOne: baseProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const item = await prisma.feature.findUnique({
where: { id: input.id },
});
if (!item) {
throw new Error("Item not found");
}
// Check ownership
if (item.userId !== ctx.user.id) {
throw new Error("Unauthorized");
}
return item;
}),
// Create
create: baseProcedure
.input(featureInputSchema)
.mutation(async ({ input, ctx }) => {
return prisma.feature.create({
data: {
...input,
userId: ctx.user.id,
},
});
}),
// Update
update: baseProcedure
.input(featureInputSchema.extend({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
// Verify ownership
const existing = await prisma.feature.findUnique({
where: { id },
});
if (!existing || existing.userId !== ctx.user.id) {
throw new Error("Unauthorized");
}
return prisma.feature.update({
where: { id },
data,
});
}),
// Delete
remove: baseProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
// Verify ownership
const existing = await prisma.feature.findUnique({
where: { id: input.id },
});
if (!existing || existing.userId !== ctx.user.id) {
throw new Error("Unauthorized");
}
return prisma.feature.delete({
where: { id: input.id },
});
}),
});
C. Register Router in App Router
// src/trpc/routers/_app.ts
import { createTRPCRouter } from "../init";
import { featureRouter } from "@/features/feature-name/server/router";
export const appRouter = createTRPCRouter({
feature: featureRouter, // Add your feature router
});
export type AppRouter = typeof appRouter;
D. Create Search Params Loader
// src/features/feature-name/server/params.ts
import { createSearchParamsCache, parseAsInteger, parseAsString } from "nuqs/server";
import { PAGINATION } from "@/lib/constants";
export const featureParamsCache = createSearchParamsCache({
page: parseAsInteger.withDefault(PAGINATION.DEFAULT_PAGE),
pageSize: parseAsInteger.withDefault(PAGINATION.DEFAULT_PAGE_SIZE),
search: parseAsString.withDefault(""),
});
export type FeatureParams = ReturnType<typeof featureParamsCache.parse>;
export async function featureParamsLoader(searchParams: Promise<any>) {
return featureParamsCache.parse(await searchParams);
}
E. Create Prefetch Helper
// src/features/feature-name/server/prefetch.ts
import type { inferInput } from "@trpc/tanstack-react-query";
import { prefetch, trpc } from "@/trpc/server";
type PrefetchFeaturesInput = inferInput<typeof trpc.feature.getMany>;
export async function prefetchFeatures(input: PrefetchFeaturesInput) {
return prefetch(trpc.feature.getMany.queryOptions(input));
}
type PrefetchFeatureInput = inferInput<typeof trpc.feature.getOne>;
export async function prefetchFeature(input: PrefetchFeatureInput) {
return prefetch(trpc.feature.getOne.queryOptions(input));
}
F. Create Client Hooks
// src/features/feature-name/hooks/use-features.ts
"use client";
import { useTRPC } from "@/trpc/client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
// Define params type inline - don't import server-side types into client
type FeaturesParams = {
page: number;
pageSize: number;
search: string;
};
export function useFeatures(params: FeaturesParams) {
const trpc = useTRPC();
return useQuery(trpc.feature.getMany.queryOptions(params));
}
export function useFeature(id: string | null, enabled: boolean = true) {
const trpc = useTRPC();
return useQuery({
...trpc.feature.getOne.queryOptions({ id: id || "" }),
enabled: enabled && !!id,
});
}
export function useCreateFeature() {
const trpc = useTRPC();
const queryClient = useQueryClient();
return useMutation(
trpc.feature.create.mutationOptions({
onSuccess: (data) => {
toast.success(`Feature "${data.name}" created successfully`);
queryClient.invalidateQueries(
trpc.feature.getMany.queryOptions({ page: 1, pageSize: 20, search: "" })
);
},
onError: (error) => {
toast.error(`Failed to create feature: ${error.message}`);
},
})
);
}
export function useUpdateFeature() {
const trpc = useTRPC();
const queryClient = useQueryClient();
return useMutation(
trpc.feature.update.mutationOptions({
onSuccess: (data) => {
toast.success(`Feature "${data.name}" updated successfully`);
queryClient.invalidateQueries(
trpc.feature.getMany.queryOptions({ page: 1, pageSize: 20, search: "" })
);
queryClient.invalidateQueries(
trpc.feature.getOne.queryOptions({ id: data.id })
);
},
onError: (error) => {
toast.error(`Failed to update feature: ${error.message}`);
},
})
);
}
export function useDeleteFeature() {
const trpc = useTRPC();
const queryClient = useQueryClient();
return useMutation(
trpc.feature.remove.mutationOptions({
onSuccess: () => {
toast.success("Feature deleted successfully");
queryClient.invalidateQueries(
trpc.feature.getMany.queryOptions({ page: 1, pageSize: 20, search: "" })
);
},
onError: (error) => {
toast.error(`Failed to delete feature: ${error.message}`);
},
})
);
}
G. Create Components
List Component (Client):
// src/features/feature-name/components/feature-list.tsx
"use client";
import { useFeatures } from "../hooks/use-features";
import { useFeatureParams } from "../hooks/use-feature-params";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export function FeatureList() {
const params = useFeatureParams();
const { data, isLoading } = useFeatures(params);
if (isLoading) return <FeatureListLoading />;
if (!data) return <FeatureListError />;
return (
<div className="space-y-4">
{data.items.map((item) => (
<div key={item.id} className="border rounded-lg p-4">
<h3 className="font-semibold">{item.name}</h3>
<p className="text-muted-foreground">{item.description}</p>
<Link href={`/features/${item.id}`}>
<Button variant="outline" size="sm">View</Button>
</Link>
</div>
))}
</div>
);
}
Form Component (Client):
// src/features/feature-name/components/feature-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useCreateFeature, useUpdateFeature } from "../hooks/use-features";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
});
type FormValues = z.infer<typeof formSchema>;
type Props = {
feature?: { id: string; name: string; description?: string | null };
onSuccess?: () => void;
};
export function FeatureForm({ feature, onSuccess }: Props) {
const isEditing = !!feature;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: feature?.name ?? "",
description: feature?.description ?? "",
},
});
const createMutation = useCreateFeature();
const updateMutation = useUpdateFeature();
const onSubmit = (data: FormValues) => {
if (isEditing) {
updateMutation.mutate(
{ id: feature.id, ...data },
{ onSuccess }
);
} else {
createMutation.mutate(data, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
});
}
};
const isPending = createMutation.isPending || updateMutation.isPending;
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} disabled={isPending} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : isEditing ? "Update" : "Create"}
</Button>
</form>
</Form>
);
}
H. Create Pages (Server Components)
List Page:
// src/app/features/page.tsx
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { HydrateClient } from "@/trpc/server";
import { FeatureList } from "@/features/feature-name/components/feature-list";
import { FeatureListLoading } from "@/features/feature-name/components/feature-list-loading";
import { FeatureListError } from "@/features/feature-name/components/feature-list-error";
import { featureParamsLoader } from "@/features/feature-name/server/params";
import { prefetchFeatures } from "@/features/feature-name/server/prefetch";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import type { SearchParams } from "nuqs/server";
type Props = {
searchParams: Promise<SearchParams>;
};
export default async function FeaturesPage({ searchParams }: Props) {
const params = await featureParamsLoader(searchParams);
prefetchFeatures(params);
return (
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold">Features</h1>
<p className="text-muted-foreground mt-2">Manage your features</p>
</div>
<Link href="/features/create">
<Button>Create New</Button>
</Link>
</div>
<HydrateClient>
<ErrorBoundary fallback={<FeatureListError />}>
<Suspense fallback={<FeatureListLoading />}>
<FeatureList />
</Suspense>
</ErrorBoundary>
</HydrateClient>
</div>
);
}
2. Adding Authentication to a Feature
Pattern: Use Better Auth context in tRPC procedures
// src/trpc/init.ts - Define protected procedure
import { initTRPC, TRPCError } from "@trpc/server";
import { auth } from "@/lib/auth";
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth.api.getSession({
headers: opts.headers,
});
return {
session,
user: session?.user,
};
};
const t = initTRPC.context<typeof createTRPCContext>().create();
export const baseProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(async (opts) => {
if (!opts.ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return opts.next({
ctx: {
...opts.ctx,
user: opts.ctx.user, // Non-null user
},
});
});
Usage:
export const featureRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
// ctx.user is guaranteed to exist
return prisma.feature.findMany({
where: { userId: ctx.user.id },
});
}),
});
3. Adding Real-time Updates
Pattern: Use React Query polling or WebSockets
// Simple polling approach
export function useFeatures(params: FeatureParams) {
return trpc.feature.getMany.useQuery(params, {
refetchInterval: 30000, // Poll every 30 seconds
});
}
// Optimistic updates for instant UI
export function useCreateFeature() {
const utils = trpc.useUtils();
return trpc.feature.create.useMutation({
onMutate: async (newFeature) => {
// Cancel outgoing refetches
await utils.feature.getMany.cancel();
// Snapshot previous value
const previous = utils.feature.getMany.getData();
// Optimistically update
utils.feature.getMany.setData(undefined, (old) => {
if (!old) return old;
return {
...old,
items: [{ id: "temp", ...newFeature }, ...old.items],
};
});
return { previous };
},
onError: (err, newFeature, context) => {
// Rollback on error
utils.feature.getMany.setData(undefined, context?.previous);
},
onSettled: () => {
// Refetch to ensure consistency
utils.feature.getMany.invalidate();
},
});
}
4. File Upload Pattern
Architecture:
Client Upload → Next.js API Route → Storage (S3/Cloudinary) → Save URL in DB
Implementation:
// src/app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function POST(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get("file") as File;
// Upload to storage (S3, Cloudinary, etc.)
const url = await uploadToStorage(file);
return NextResponse.json({ url });
}
5. Background Jobs Pattern
Pattern: Use Next.js API routes with queues (BullMQ, etc.)
// src/app/api/jobs/process/route.ts
export async function POST(request: Request) {
const { jobType, data } = await request.json();
// Add to queue
await queue.add(jobType, data);
return Response.json({ status: "queued" });
}
// Trigger from tRPC mutation
export const featureRouter = createTRPCRouter({
processLongTask: protectedProcedure
.input(z.object({ data: z.string() }))
.mutation(async ({ input }) => {
// Trigger background job
await fetch("/api/jobs/process", {
method: "POST",
body: JSON.stringify({
jobType: "processTask",
data: input.data,
}),
});
return { status: "processing" };
}),
});
Architectural Decision Framework
When designing a new feature, consider:
1. Data Flow
- Where does data originate? (User input, external API, database)
- Where is it processed? (Server component, API route, tRPC procedure)
- Where is it stored? (PostgreSQL via Prisma)
- How is it displayed? (Client component with React Query)
2. Component Boundaries
- What needs to be server? (Data fetching, authentication checks)
- What needs to be client? (Interactivity, forms, real-time updates)
- What's shared? (Types, constants, utilities)
3. State Management
- Server state (API data) → TanStack React Query via tRPC
- Client state (UI state) → React useState/useReducer
- Form state → React Hook Form
- URL state (filters, pagination) → nuqs (useQueryState)
4. Security
- Authentication: Use Better Auth session
- Authorization: Check ownership in tRPC procedures
- Input validation: Zod schemas on client AND server
- Data exposure: Never expose sensitive data to client
5. Performance
- Server-side rendering: Prefetch data in server components
- Client-side caching: React Query handles this automatically
- Database queries: Use indexes, avoid N+1 queries
- Bundle size: Use dynamic imports for heavy components
6. Error Handling
- Server errors: Throw descriptive errors in tRPC procedures
- Client errors: Error boundaries + error state components
- Form errors: Zod validation errors displayed via FormMessage
- Network errors: React Query retry logic
Common Architecture Questions
Q: Should this be a server or client component?
Decision Tree:
- Does it need interactivity (onClick, onChange)? → Client
- Does it use React hooks (useState, useEffect)? → Client
- Does it access browser APIs? → Client
- Does it fetch data or check auth? → Server
- Is it just rendering? → Server (default)
Q: Where should business logic live?
- Data validation: Zod schemas (shared between client and server)
- Data transformation: tRPC procedures (server-side)
- Database queries: tRPC procedures (server-side)
- UI logic: Client components
- Routing logic: Server components / App Router
Q: How do I share code between features?
- Components: Extract to
src/components/app/ - Utilities: Add to
src/lib/utils/ - Hooks: Add to
src/lib/hooks/(if truly generic) - Types: Define in feature, export if needed elsewhere
- Constants: Add to
src/lib/constants.ts
Q: When should I create a new tRPC router vs add to existing?
- New feature domain: Create new router in
features/[name]/server/router.ts - Related functionality: Add to existing feature router
- Cross-cutting concern: Consider if it belongs in a shared router
Design Checklist
Before implementing a new feature:
- Database schema designed with proper relations and indexes
- tRPC router with input validation (Zod schemas)
- Router registered in
src/trpc/routers/_app.ts - Search params parser if needed (nuqs)
- Prefetch helpers for server-side data loading
- Client hooks for data fetching and mutations
- Loading and error state components
- Form components with validation
- Server component pages with proper boundaries
- Authentication and authorization checks
- Error handling at all layers
- Type safety verified throughout
Anti-Patterns to Avoid
❌ Don't:
- Use client components for everything
- Fetch data in client components (use tRPC queries)
- Put server code (Prisma) in client components
- Skip input validation
- Expose sensitive data to client
- Create deeply nested component hierarchies
- Mix concerns in single files
- Use
anytypes - Forget to add database indexes
- Hardcode values that should be constants
✅ Do:
- Default to server components
- Use tRPC for all API calls
- Validate inputs with Zod
- Keep features isolated in feature folders
- Use TypeScript strictly
- Follow established file structure
- Add loading and error states
- Consider mobile responsiveness
- Think about accessibility
- Write maintainable, readable code
Your role is to guide developers through architectural decisions, provide code examples that follow these patterns, and ensure consistency across the codebase. When asked about implementing a feature, break it down into these layers and provide concrete implementation guidance.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon