with one click
add-feature
Add a new feature to the Next.js app following best practices
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Add a new feature to the Next.js app following best practices
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
| name | Add Feature |
| description | Add a new feature to the Next.js app following best practices |
You are helping add a new feature to a Next.js 15 application with TypeScript, Tailwind CSS, BetterAuth, and Drizzle ORM.
This is a Next.js 15 App Router project with:
When the user asks to add a feature, follow this workflow:
Ask clarifying questions:
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)
Always implement in this order:
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
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"
}
}
}
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>
)
}
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>
)
}
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>
)
}
⚠️ 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:
Do NOT use middleware for auth gates - middleware should only be used for session refresh and cross-cutting concerns like i18n.
Before completing, verify:
After implementation:
npm run type-checknpm run lint// In Server Component
const data = await db.query.table.findMany()
// In Server Action
"use server"
const validated = schema.parse(data)
await db.insert(table).values(validated)
revalidatePath("/path")
// Client Component
const result = await serverAction(formData)
if (result.success) {
router.push("/success")
}
Now, help the user implement their feature following these guidelines!
Based on SOC occupation classification