
add-feature
by gregsantos
SKILL.md
name: Add Feature description: Add a new feature to the Next.js app following best practices
Add Feature Skill
You are helping add a new feature to a Next.js 15 application with TypeScript, Tailwind CSS, BetterAuth, and Drizzle ORM.
Context
This is a Next.js 15 App Router project with:
- Server Components by default
- Drizzle ORM for database (PostgreSQL)
- BetterAuth for authentication
- TypeScript strict mode
- Tailwind CSS v4 + shadcn/ui
Your Task
When the user asks to add a feature, follow this workflow:
1. Understand Requirements
Ask clarifying questions:
- What routes are needed?
- Is authentication required?
- Does it need database tables?
- What data needs to be displayed?
- What user interactions are needed?
2. Plan Architecture
Before coding, outline:
Route Structure:
- app/[feature-name]/page.tsx (main page)
- app/[feature-name]/layout.tsx (if needed)
- app/[feature-name]/loading.tsx (loading state)
- app/[feature-name]/error.tsx (error boundary)
Database Schema:
- lib/server/db/schema/[feature].ts (if new tables needed)
Server Actions:
- lib/actions/[feature].ts (for mutations)
Components:
- components/[feature]/ (feature-specific components)
3. Implementation Order
Always implement in this order:
Step 1: Database Schema (if needed)
Create schema file:
// lib/server/db/schema/[feature].ts
import { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core"
import { createInsertSchema, createSelectSchema } from "drizzle-zod"
export const [tableName] = pgTable("[table_name]", {
id: uuid("id").primaryKey().defaultRandom(),
// Add fields here
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
})
export const insert[TableName]Schema = createInsertSchema([tableName])
export const select[TableName]Schema = createSelectSchema([tableName])
export type [TableName] = typeof [tableName].$inferSelect
export type New[TableName] = typeof [tableName].$inferInsert
Export from schema/index.ts:
export * from "./[feature]"
Run migration:
npm run db:push
Step 2: Server Actions (if mutations needed)
Create server actions:
// lib/actions/[feature].ts
"use server"
import { db } from "@/lib/server/db"
import { [tableName], insert[TableName]Schema } from "@/lib/server/db/schema"
import { revalidatePath } from "next/cache"
import { z } from "zod"
export async function create[Resource](data: z.infer<typeof insert[TableName]Schema>) {
try {
const validated = insert[TableName]Schema.parse(data)
const [created] = await db.insert([tableName]).values(validated).returning()
revalidatePath("/[feature-path]")
return { success: true, data: created }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error"
}
}
}
Step 3: Page Components
Create the main page (Server Component):
// app/[feature]/page.tsx
import { db } from "@/lib/server/db"
import { [tableName] } from "@/lib/server/db/schema"
export const metadata = {
title: "[Feature Title]",
description: "[Feature description]",
}
export default async function [Feature]Page() {
// Fetch data in Server Component
const data = await db.query.[tableName].findMany()
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold">[Feature Title]</h1>
{/* Render data */}
</div>
)
}
Step 4: Interactive Components (Client Components)
Only create Client Components when needed:
// components/[feature]/[component-name].tsx
"use client"
import { useState } from "react"
import { create[Resource] } from "@/lib/actions/[feature]"
import { Button } from "@/components/ui/button"
export function [ComponentName]() {
const [isLoading, setIsLoading] = useState(false)
async function handleAction() {
setIsLoading(true)
const result = await create[Resource](data)
setIsLoading(false)
}
return (
<div>
<Button onClick={handleAction} disabled={isLoading}>
{isLoading ? "Loading..." : "Action"}
</Button>
</div>
)
}
Step 5: Loading & Error States
Create loading.tsx:
// app/[feature]/loading.tsx
export default function Loading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-64 animate-pulse rounded bg-muted" />
</div>
)
}
Create error.tsx:
// app/[feature]/error.tsx
"use client"
export default function Error({ error, reset }: {
error: Error
reset: () => void
}) {
return (
<div className="container mx-auto p-8">
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Step 6: Authentication (if needed)
⚠️ Always use server layouts for auth, NOT middleware.
Add auth layout with forced dynamic rendering:
// app/[feature]/layout.tsx
import { redirect } from "next/navigation"
import { auth } from "@/lib/server/auth"
import { headers } from "next/headers"
// ⚠️ CRITICAL: Force dynamic rendering for fresh auth checks
export const dynamic = "force-dynamic"
export default async function [Feature]Layout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth.api.getSession({
headers: await headers(),
})
if (!session) {
redirect("/auth/signin")
}
return <>{children}</>
}
Why server layouts for auth:
- ✅ Execute on the same request (no extra hops)
- ✅ Full TypeScript support and type inference
- ✅ Can render UI and compose with data fetching
- ✅ Co-located with protected routes
Do NOT use middleware for auth gates - middleware should only be used for session refresh and cross-cutting concerns like i18n.
4. Best Practices Checklist
Before completing, verify:
- Server Components by default
- Client Components only when needed
- Type safety (TypeScript + Zod validation)
- Error handling in server actions
- Loading states (loading.tsx)
- Error boundaries (error.tsx)
- Metadata for SEO
- Revalidation after mutations
- Mobile-responsive design
- Accessibility (ARIA attributes, semantic HTML)
5. Testing
After implementation:
- Test the feature manually
- Verify TypeScript compilation:
npm run type-check - Check for lint errors:
npm run lint - Test all user flows
- Verify error states
- Test loading states
Key Principles
- Server-First: Always start with Server Components
- Type Safety: Use TypeScript and Zod everywhere
- Co-location: Keep related code together
- Performance: Minimize client-side JavaScript
- User Experience: Always show loading and error states
- Security: Validate all inputs, check permissions
Common Patterns
Data Fetching Pattern
// In Server Component
const data = await db.query.table.findMany()
Mutation Pattern
// In Server Action
"use server"
const validated = schema.parse(data)
await db.insert(table).values(validated)
revalidatePath("/path")
Form Pattern
// Client Component
const result = await serverAction(formData)
if (result.success) {
router.push("/success")
}
Now, help the user implement their feature following these guidelines!
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon