The modern web development landscape demands efficiency, type safety, and rapid prototyping capabilities. The combination of Next.js, Shadcn/UI, TypeScript, and BetterAuth represents one of the most powerful and developer-friendly stacks available today. When enhanced with intelligent Cursor Rules, this stack becomes a productivity powerhouse that can accelerate your development workflow by 10x.
This technology stack has become the go-to choice for modern web applications, and for good reason:
Without intelligent assistance, working with this stack involves:
With proper Cursor Rules, you achieve:
ā Developer Experience Advantages
- š Instant Setup: Automated project scaffolding with best practices
- šÆ Smart Imports: Automatic import suggestions for components and utilities
- š± Component Intelligence: Context-aware Shadcn component suggestions
- š Auth Patterns: Pre-built authentication flows and middleware
- š”ļø Type Safety: Comprehensive TypeScript coverage with minimal effort
Next.js Page Component Rules (TypeScript)
{
"name": "Next.js App Router Page",
"description": "Generate App Router page components with TypeScript",
"trigger": "nextpage",
"expansion": "export default function ${1:PageName}({ params, searchParams }: { params: { ${2:slug}: string }, searchParams: { [key: string]: string | string[] | undefined } }) {\n return (\n <div className=\"container mx-auto py-8\">\n <h1 className=\"text-3xl font-bold\">${3:Page Title}</h1>\n $0\n </div>\n )\n}",
"scope": ["typescript", "tsx"]
}
Next.js API Route Rules (TypeScript)
{
"name": "Next.js API Route",
"description": "Generate typed API route handlers",
"trigger": "nextapi",
"expansion": "import { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n try {\n $1\n return NextResponse.json({ success: true })\n } catch (error) {\n return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })\n }\n}\n\nexport async function POST(request: NextRequest) {\n try {\n const body = await request.json()\n $2\n return NextResponse.json({ success: true })\n } catch (error) {\n return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })\n }\n}",
"scope": ["typescript"]
}
Server Component Rules (TypeScript)
{
"name": "Next.js Server Component",
"description": "Generate async server components with proper typing",
"trigger": "servercomp",
"expansion": "interface ${1:Component}Props {\n ${2:id}: string\n}\n\nexport default async function ${1:Component}({ ${2:id} }: ${1:Component}Props) {\n // Server-side data fetching\n const data = await fetch${3:Data}(${2:id})\n \n return (\n <div>\n {/* Server-rendered content */}\n $0\n </div>\n )\n}",
"scope": ["typescript", "tsx"]
}
Client Component Rules (TypeScript)
{
"name": "Next.js Client Component",
"description": "Generate client components with hooks",
"trigger": "clientcomp",
"expansion": "'use client'\n\nimport { useState } from 'react'\n\ninterface ${1:Component}Props {\n ${2:initialValue}?: string\n}\n\nexport default function ${1:Component}({ ${2:initialValue} }: ${1:Component}Props) {\n const [${3:state}, set${3:State}] = useState(${2:initialValue} || '')\n \n return (\n <div>\n {/* Client-side interactive content */}\n $0\n </div>\n )\n}",
"scope": ["typescript", "tsx"]
}
Shadcn Form Rules (TypeScript)
{
"name": "Shadcn Form with Validation",
"description": "Generate forms with react-hook-form and zod validation",
"trigger": "shadform",
"expansion": "import { zodResolver } from '@hookform/resolvers/zod'\nimport { useForm } from 'react-hook-form'\nimport * as z from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'\nimport { Input } from '@/components/ui/input'\n\nconst formSchema = z.object({\n ${1:fieldName}: z.string().min(${2:2}, {\n message: '${3:Field must be at least 2 characters.}',\n }),\n})\n\nexport function ${4:FormName}() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n ${1:fieldName}: '',\n },\n })\n\n function onSubmit(values: z.infer<typeof formSchema>) {\n console.log(values)\n }\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-8\">\n <FormField\n control={form.control}\n name=\"${1:fieldName}\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>${5:Field Label}</FormLabel>\n <FormControl>\n <Input placeholder=\"${6:Enter value}\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <Button type=\"submit\">Submit</Button>\n </form>\n </Form>\n )\n}",
"scope": ["typescript", "tsx"]
}
Shadcn Data Table Rules (TypeScript)
{
"name": "Shadcn Data Table",
"description": "Generate data tables with sorting and filtering",
"trigger": "shadtable",
"expansion": "import {\n ColumnDef,\n flexRender,\n getCoreRowModel,\n useReactTable,\n} from '@tanstack/react-table'\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@/components/ui/table'\n\ninterface ${1:DataType} {\n id: string\n ${2:name}: string\n ${3:email}: string\n}\n\ninterface DataTableProps {\n columns: ColumnDef<${1:DataType}>[]\n data: ${1:DataType}[]\n}\n\nexport function DataTable({ columns, data }: DataTableProps) {\n const table = useReactTable({\n data,\n columns,\n getCoreRowModel: getCoreRowModel(),\n })\n\n return (\n <div className=\"rounded-md border\">\n <Table>\n <TableHeader>\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => {\n return (\n <TableHead key={header.id}>\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n </TableHead>\n )\n })}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) => (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() && 'selected'}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : (\n <TableRow>\n <TableCell colSpan={columns.length} className=\"h-24 text-center\">\n No results.\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n )\n}",
"scope": ["typescript", "tsx"]
}
Shadcn Dialog Rules (TypeScript)
{
"name": "Shadcn Dialog Component",
"description": "Generate modal dialogs with proper state management",
"trigger": "shaddialog",
"expansion": "import {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from '@/components/ui/dialog'\nimport { Button } from '@/components/ui/button'\n\ninterface ${1:Dialog}Props {\n open?: boolean\n onOpenChange?: (open: boolean) => void\n ${2:title}: string\n ${3:description}?: string\n children: React.ReactNode\n}\n\nexport function ${1:Dialog}({\n open,\n onOpenChange,\n ${2:title},\n ${3:description},\n children,\n}: ${1:Dialog}Props) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogTrigger asChild>\n <Button variant=\"outline\">${4:Open Dialog}</Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-[425px]\">\n <DialogHeader>\n <DialogTitle>{${2:title}}</DialogTitle>\n {${3:description} && (\n <DialogDescription>\n {${3:description}}\n </DialogDescription>\n )}\n </DialogHeader>\n <div className=\"grid gap-4 py-4\">\n {children}\n </div>\n <DialogFooter>\n <Button type=\"submit\">${5:Save changes}</Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}",
"scope": ["typescript", "tsx"]
}
TypeScript Interface Rules
{
"name": "TypeScript Interface",
"description": "Generate comprehensive interfaces with optional properties",
"trigger": "tsinterface",
"expansion": "interface ${1:InterfaceName} {\n id: string\n ${2:name}: string\n ${3:email}?: string\n ${4:createdAt}: Date\n ${5:updatedAt}: Date\n}",
"scope": ["typescript", "tsx"]
}
API Response Types Rules
{
"name": "API Response Types",
"description": "Generate typed API response interfaces",
"trigger": "apitype",
"expansion": "interface ${1:Resource} {\n id: string\n ${2:attributes}: string\n createdAt: string\n updatedAt: string\n}\n\ninterface ${1:Resource}Response {\n data: ${1:Resource}\n message?: string\n}\n\ninterface ${1:Resource}ListResponse {\n data: ${1:Resource}[]\n pagination: {\n page: number\n limit: number\n total: number\n totalPages: number\n }\n message?: string\n}\n\ninterface ApiError {\n error: string\n message: string\n statusCode: number\n}",
"scope": ["typescript", "tsx"]
}
Custom Hook Rules (TypeScript)
{
"name": "Custom React Hook",
"description": "Generate typed custom hooks with proper return types",
"trigger": "tshook",
"expansion": "import { useState, useEffect } from 'react'\n\ninterface Use${1:Hook}Options {\n ${2:option}?: string\n}\n\ninterface Use${1:Hook}Return {\n ${3:data}: ${4:string} | null\n ${5:loading}: boolean\n ${6:error}: string | null\n ${7:refetch}: () => void\n}\n\nexport function use${1:Hook}(options: Use${1:Hook}Options = {}): Use${1:Hook}Return {\n const [${3:data}, set${3:Data}] = useState<${4:string} | null>(null)\n const [${5:loading}, set${5:Loading}] = useState(false)\n const [${6:error}, set${6:Error}] = useState<string | null>(null)\n\n const fetch${3:Data} = async () => {\n set${5:Loading}(true)\n set${6:Error}(null)\n \n try {\n // Implement fetch logic\n const result = await fetch${8:API}(options.${2:option})\n set${3:Data}(result)\n } catch (err) {\n set${6:Error}(err instanceof Error ? err.message : 'An error occurred')\n } finally {\n set${5:Loading}(false)\n }\n }\n\n useEffect(() => {\n fetch${3:Data}()\n }, [options.${2:option}])\n\n return {\n ${3:data},\n ${5:loading},\n ${6:error},\n ${7:refetch}: fetch${3:Data},\n }\n}",
"scope": ["typescript", "tsx"]
}
BetterAuth Configuration Rules
{
"name": "BetterAuth Configuration",
"description": "Generate BetterAuth setup with providers",
"trigger": "betterauth",
"expansion": "import { betterAuth } from 'better-auth'\nimport { prismaAdapter } from 'better-auth/adapters/prisma'\nimport { prisma } from '@/lib/prisma'\n\nexport const auth = betterAuth({\n database: prismaAdapter(prisma, {\n provider: 'postgresql', // or 'mysql', 'sqlite'\n }),\n socialProviders: {\n github: {\n clientId: process.env.GITHUB_CLIENT_ID as string,\n clientSecret: process.env.GITHUB_CLIENT_SECRET as string,\n },\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID as string,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,\n },\n },\n user: {\n additionalFields: {\n ${1:role}: {\n type: 'string',\n required: false,\n },\n },\n },\n session: {\n expiresIn: 60 * 60 * 24 * 7, // 7 days\n updateAge: 60 * 60 * 24, // 1 day\n },\n})",
"scope": ["typescript"]
}
Auth Client Hook Rules
{
"name": "BetterAuth Client Hook",
"description": "Generate client-side auth hooks",
"trigger": "authclient",
"expansion": "import { createAuthClient } from 'better-auth/react'\n\nexport const authClient = createAuthClient({\n baseURL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',\n})\n\nexport const {\n signIn,\n signOut,\n signUp,\n useSession,\n} = authClient",
"scope": ["typescript"]
}
Protected Route Component Rules
{
"name": "Protected Route Component",
"description": "Generate protected route wrapper with auth checks",
"trigger": "protectedroute",
"expansion": "'use client'\n\nimport { useSession } from '@/lib/auth-client'\nimport { useRouter } from 'next/navigation'\nimport { useEffect } from 'react'\nimport { Loader2 } from 'lucide-react'\n\ninterface ProtectedRouteProps {\n children: React.ReactNode\n ${1:requiredRole}?: string\n}\n\nexport function ProtectedRoute({ children, ${1:requiredRole} }: ProtectedRouteProps) {\n const { data: session, isPending } = useSession()\n const router = useRouter()\n\n useEffect(() => {\n if (!isPending && !session) {\n router.push('/auth/signin')\n return\n }\n\n if (${1:requiredRole} && session?.user?.${2:role} !== ${1:requiredRole}) {\n router.push('/unauthorized')\n return\n }\n }, [session, isPending, ${1:requiredRole}, router])\n\n if (isPending) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n )\n }\n\n if (!session || (${1:requiredRole} && session.user?.${2:role} !== ${1:requiredRole})) {\n return null\n }\n\n return <>{children}</>\n}",
"scope": ["typescript", "tsx"]
}
Sign In Form Rules
{
"name": "BetterAuth Sign In Form",
"description": "Generate sign-in form with social providers",
"trigger": "signinform",
"expansion": "'use client'\n\nimport { useState } from 'react'\nimport { signIn } from '@/lib/auth-client'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Github } from 'lucide-react'\n\nexport function SignInForm() {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n\n const handleEmailSignIn = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n \n try {\n await signIn.email({\n email,\n password,\n })\n } catch (error) {\n console.error('Sign in error:', error)\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: '${1:github}' | '${2:google}') => {\n try {\n await signIn.social({\n provider,\n callbackURL: '/dashboard',\n })\n } catch (error) {\n console.error('Social sign in error:', error)\n }\n }\n\n return (\n <Card className=\"w-full max-w-md mx-auto\">\n <CardHeader>\n <CardTitle>Sign In</CardTitle>\n <CardDescription>\n Enter your email below to sign in to your account\n </CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <form onSubmit={handleEmailSignIn} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Signing in...' : 'Sign In'}\n </Button>\n </form>\n \n <div className=\"relative\">\n <div className=\"absolute inset-0 flex items-center\">\n <span className=\"w-full border-t\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-background px-2 text-muted-foreground\">\n Or continue with\n </span>\n </div>\n </div>\n \n <div className=\"grid grid-cols-2 gap-4\">\n <Button\n variant=\"outline\"\n onClick={() => handleSocialSignIn('${1:github}')}\n >\n <Github className=\"mr-2 h-4 w-4\" />\n GitHub\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => handleSocialSignIn('${2:google}')}\n >\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n fill=\"#4285F4\"\n />\n <path\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n fill=\"#34A853\"\n />\n <path\n d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n fill=\"#FBBC05\"\n />\n <path\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n fill=\"#EA4335\"\n />\n </svg>\n Google\n </Button>\n </div>\n </CardContent>\n </Card>\n )\n}",
"scope": ["typescript", "tsx"]
}
Let's build a comprehensive product dashboard using our tech stack with Cursor Rules assistance:
Product Dashboard Component (TSX)
'use client'
import { useState, useEffect } from 'react'
import { useSession } from '@/lib/auth-client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Plus, Search, Filter } from 'lucide-react'
interface Product {
id: string
name: string
price: number
category: string
status: 'active' | 'inactive' | 'draft'
createdAt: string
}
export default function ProductDashboard() {
const { data: session } = useSession()
const [products, setProducts] = useState<Product[]>([])
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchProducts()
}, [])
const fetchProducts = async () => {
try {
const response = await fetch('/api/products')
const data = await response.json()
setProducts(data.products)
} catch (error) {
console.error('Failed to fetch products:', error)
} finally {
setLoading(false)
}
}
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
)
if (loading) {
return <div className="flex items-center justify-center p-8">Loading...</div>
}
return (
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold">Products</h1>
<p className="text-muted-foreground">Manage your product inventory</p>
</div>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Product
</Button>
</div>
<div className="flex gap-4 mb-6">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Button variant="outline">
<Filter className="mr-2 h-4 w-4" />
Filter
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProducts.map((product) => (
<Card key={product.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="line-clamp-1">{product.name}</CardTitle>
<Badge variant={product.status === 'active' ? 'default' : 'secondary'}>
{product.status}
</Badge>
</div>
<CardDescription>${product.price.toFixed(2)}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">{product.category}</span>
<Button variant="outline" size="sm">
Edit
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{filteredProducts.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">No products found</p>
</div>
)}
</div>
)
}
nextpage
ā Generated the page component structuretshook
ā Created the custom hook for data fetchingshadtable
ā Added the data table functionalityprotectedroute
ā Wrapped with authenticationUser Profile Form Component (TSX)
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { useSession } from '@/lib/auth-client'
import { Button } from '@/components/ui/button'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { toast } from '@/components/ui/use-toast'
const profileFormSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
bio: z.string().max(500, 'Bio must be less than 500 characters').optional(),
website: z.string().url('Invalid URL').optional().or(z.literal('')),
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
export default function UserProfile() {
const { data: session } = useSession()
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues: {
name: session?.user?.name || '',
email: session?.user?.email || '',
bio: session?.user?.bio || '',
website: session?.user?.website || '',
},
})
async function onSubmit(data: ProfileFormValues) {
try {
const response = await fetch('/api/user/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (response.ok) {
toast({
title: 'Profile updated',
description: 'Your profile has been updated successfully.',
})
} else {
throw new Error('Failed to update profile')
}
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update profile. Please try again.',
variant: 'destructive',
})
}
}
return (
<div className="container mx-auto py-8 max-w-2xl">
<Card>
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
<CardDescription>
Update your profile information and preferences.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage src={session?.user?.image} alt={session?.user?.name} />
<AvatarFallback>
{session?.user?.name?.charAt(0)?.toUpperCase()}
</AvatarFallback>
</Avatar>
<Button variant="outline">Change Avatar</Button>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="Enter your full name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter your email" type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>Website</FormLabel>
<FormControl>
<Input placeholder="https://your-website.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Update Profile
</Button>
</form>
</Form>
</CardContent>
</Card>
</div>
)
}
Authentication Middleware Rules
{
"name": "Next.js Auth Middleware",
"description": "Generate middleware for route protection",
"trigger": "authmiddleware",
"expansion": "import { NextRequest, NextResponse } from 'next/server'\nimport { auth } from '@/lib/auth'\n\nexport async function middleware(request: NextRequest) {\n const { pathname } = request.nextUrl\n \n // Public routes that don't require authentication\n const publicRoutes = ['/auth/signin', '/auth/signup', '/api/auth', '/']\n \n if (publicRoutes.some(route => pathname.startsWith(route))) {\n return NextResponse.next()\n }\n \n // Check for authentication\n const session = await auth.api.getSession({\n headers: request.headers,\n })\n \n if (!session) {\n const signInUrl = new URL('/auth/signin', request.url)\n signInUrl.searchParams.set('callbackUrl', pathname)\n return NextResponse.redirect(signInUrl)\n }\n \n // Role-based route protection\n const adminRoutes = ['/admin']\n if (adminRoutes.some(route => pathname.startsWith(route))) {\n if (session.user.role !== 'admin') {\n return NextResponse.redirect(new URL('/unauthorized', request.url))\n }\n }\n \n return NextResponse.next()\n}\n\nexport const config = {\n matcher: [\n '/((?!api|_next/static|_next/image|favicon.ico).*)',\n ],\n}",
"scope": ["typescript"]
}
Prisma Schema Rules
{
"name": "Prisma Schema Models",
"description": "Generate Prisma models for BetterAuth integration",
"trigger": "prismaauth",
"expansion": "model User {\n id String @id @default(cuid())\n name String?\n email String @unique\n emailVerified DateTime?\n image String?\n role Role @default(USER)\n accounts Account[]\n sessions Session[]\n posts Post[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"users\")\n}\n\nmodel Account {\n id String @id @default(cuid())\n userId String\n type String\n provider String\n providerAccountId String\n refresh_token String? @db.Text\n access_token String? @db.Text\n expires_at Int?\n token_type String?\n scope String?\n id_token String? @db.Text\n session_state String?\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([provider, providerAccountId])\n @@map(\"accounts\")\n}\n\nmodel Session {\n id String @id @default(cuid())\n sessionToken String @unique\n userId String\n expires DateTime\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@map(\"sessions\")\n}\n\nmodel VerificationToken {\n identifier String\n token String @unique\n expires DateTime\n\n @@unique([identifier, token])\n @@map(\"verificationtokens\")\n}\n\nenum Role {\n USER\n ADMIN\n MODERATOR\n}",
"scope": ["prisma"]
}
CRUD API Routes Rules
{
"name": "CRUD API Routes",
"description": "Generate complete CRUD operations with auth",
"trigger": "crudapi",
"expansion": "import { NextRequest, NextResponse } from 'next/server'\nimport { auth } from '@/lib/auth'\nimport { prisma } from '@/lib/prisma'\nimport { z } from 'zod'\n\nconst ${1:Resource}Schema = z.object({\n ${2:name}: z.string().min(1),\n ${3:description}: z.string().optional(),\n})\n\n// GET /api/${4:resources}\nexport async function GET(request: NextRequest) {\n try {\n const session = await auth.api.getSession({\n headers: request.headers,\n })\n\n if (!session) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const ${4:resources} = await prisma.${5:resource}.findMany({\n where: {\n userId: session.user.id,\n },\n orderBy: {\n createdAt: 'desc',\n },\n })\n\n return NextResponse.json({ ${4:resources} })\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal Server Error' },\n { status: 500 }\n )\n }\n}\n\n// POST /api/${4:resources}\nexport async function POST(request: NextRequest) {\n try {\n const session = await auth.api.getSession({\n headers: request.headers,\n })\n\n if (!session) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await request.json()\n const validatedData = ${1:Resource}Schema.parse(body)\n\n const ${5:resource} = await prisma.${5:resource}.create({\n data: {\n ...validatedData,\n userId: session.user.id,\n },\n })\n\n return NextResponse.json({ ${5:resource} }, { status: 201 })\n } catch (error) {\n if (error instanceof z.ZodError) {\n return NextResponse.json(\n { error: 'Validation Error', details: error.errors },\n { status: 400 }\n )\n }\n\n return NextResponse.json(\n { error: 'Internal Server Error' },\n { status: 500 }\n )\n }\n}",
"scope": ["typescript"]
}
Dynamic Import Rules
{
"name": "Dynamic Component Import",
"description": "Generate lazy-loaded components with loading states",
"trigger": "dynamicimport",
"expansion": "import dynamic from 'next/dynamic'\nimport { Loader2 } from 'lucide-react'\n\nconst ${1:Component} = dynamic(() => import('@/components/${2:component-path}'), {\n loading: () => (\n <div className=\"flex items-center justify-center p-8\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n ),\n ssr: ${3:false},\n})",
"scope": ["typescript", "tsx"]
}
API Caching Rules
{
"name": "Next.js API Caching",
"description": "Generate cached API routes with revalidation",
"trigger": "apicache",
"expansion": "import { NextRequest, NextResponse } from 'next/server'\nimport { unstable_cache } from 'next/cache'\n\nconst getCached${1:Data} = unstable_cache(\n async (${2:id}: string) => {\n // Expensive operation\n const data = await fetch${1:Data}(${2:id})\n return data\n },\n ['${3:cache-key}'],\n {\n revalidate: ${4:3600}, // 1 hour\n tags: ['${5:tag}'],\n }\n)\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { ${2:id}: string } }\n) {\n try {\n const data = await getCached${1:Data}(params.${2:id})\n \n return NextResponse.json({ data }, {\n headers: {\n 'Cache-Control': 'public, s-maxage=${4:3600}, stale-while-revalidate=${6:86400}',\n },\n })\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal Server Error' },\n { status: 500 }\n )\n }\n}",
"scope": ["typescript"]
}
React Testing Library Rules
{
"name": "Component Test",
"description": "Generate comprehensive component tests",
"trigger": "componenttest",
"expansion": "import { render, screen, fireEvent, waitFor } from '@testing-library/react'\nimport { vi } from 'vitest'\nimport { ${1:Component} } from '@/components/${2:component-path}'\n\n// Mock dependencies\nvi.mock('@/lib/auth-client', () => ({\n useSession: vi.fn(() => ({\n data: {\n user: {\n id: '1',\n name: 'Test User',\n email: 'test@example.com',\n },\n },\n isPending: false,\n })),\n}))\n\ndescribe('${1:Component}', () => {\n it('renders correctly', () => {\n render(<${1:Component} ${3:prop}=\"${4:value}\" />)\n \n expect(screen.getByText('${5:expected-text}')).toBeInTheDocument()\n })\n\n it('handles user interaction', async () => {\n const mockHandler = vi.fn()\n \n render(<${1:Component} ${6:onAction}={mockHandler} />)\n \n fireEvent.click(screen.getByRole('button', { name: '${7:button-text}' }))\n \n await waitFor(() => {\n expect(mockHandler).toHaveBeenCalledWith(${8:expected-args})\n })\n })\n\n it('displays loading state', () => {\n render(<${1:Component} ${9:loading}={true} />)\n \n expect(screen.getByText('Loading...')).toBeInTheDocument()\n })\n\n it('handles error state', () => {\n render(<${1:Component} ${10:error}=\"${11:error-message}\" />)\n \n expect(screen.getByText('${11:error-message}')).toBeInTheDocument()\n })\n})",
"scope": ["typescript", "tsx"]
}
API Route Testing Rules
{
"name": "API Route Test",
"description": "Generate API route tests with auth mocking",
"trigger": "apitest",
"expansion": "import { NextRequest } from 'next/server'\nimport { vi } from 'vitest'\nimport { GET, POST } from '@/app/api/${1:endpoint}/route'\n\n// Mock auth\nvi.mock('@/lib/auth', () => ({\n auth: {\n api: {\n getSession: vi.fn(),\n },\n },\n}))\n\n// Mock database\nvi.mock('@/lib/prisma', () => ({\n prisma: {\n ${2:model}: {\n findMany: vi.fn(),\n create: vi.fn(),\n findUnique: vi.fn(),\n update: vi.fn(),\n delete: vi.fn(),\n },\n },\n}))\n\nconst mockSession = {\n user: {\n id: '1',\n name: 'Test User',\n email: 'test@example.com',\n },\n}\n\ndescribe('/api/${1:endpoint}', () => {\n beforeEach(() => {\n vi.clearAllMocks()\n })\n\n describe('GET', () => {\n it('returns ${2:model}s for authenticated user', async () => {\n const mockData = [{ id: '1', name: 'Test ${3:Item}' }]\n \n vi.mocked(auth.api.getSession).mockResolvedValue(mockSession)\n vi.mocked(prisma.${2:model}.findMany).mockResolvedValue(mockData)\n\n const request = new NextRequest('http://localhost:3000/api/${1:endpoint}')\n const response = await GET(request)\n const data = await response.json()\n\n expect(response.status).toBe(200)\n expect(data.${1:endpoint}).toEqual(mockData)\n })\n\n it('returns 401 for unauthenticated user', async () => {\n vi.mocked(auth.api.getSession).mockResolvedValue(null)\n\n const request = new NextRequest('http://localhost:3000/api/${1:endpoint}')\n const response = await GET(request)\n\n expect(response.status).toBe(401)\n })\n })\n\n describe('POST', () => {\n it('creates new ${3:item} for authenticated user', async () => {\n const newItem = { name: 'New ${3:Item}' }\n const createdItem = { id: '2', ...newItem }\n \n vi.mocked(auth.api.getSession).mockResolvedValue(mockSession)\n vi.mocked(prisma.${2:model}.create).mockResolvedValue(createdItem)\n\n const request = new NextRequest('http://localhost:3000/api/${1:endpoint}', {\n method: 'POST',\n body: JSON.stringify(newItem),\n })\n \n const response = await POST(request)\n const data = await response.json()\n\n expect(response.status).toBe(201)\n expect(data.${4:item}).toEqual(createdItem)\n })\n\n it('validates request body', async () => {\n vi.mocked(auth.api.getSession).mockResolvedValue(mockSession)\n\n const request = new NextRequest('http://localhost:3000/api/${1:endpoint}', {\n method: 'POST',\n body: JSON.stringify({}), // Invalid data\n })\n \n const response = await POST(request)\n\n expect(response.status).toBe(400)\n })\n })\n})",
"scope": ["typescript"]
}
Environment Variables Rules
{
"name": "Environment Configuration",
"description": "Generate environment variable schemas and validation",
"trigger": "envconfig",
"expansion": "import { z } from 'zod'\n\nconst envSchema = z.object({\n // Database\n DATABASE_URL: z.string().url(),\n \n // Authentication\n BETTER_AUTH_SECRET: z.string().min(32),\n BETTER_AUTH_URL: z.string().url(),\n \n // OAuth Providers\n GITHUB_CLIENT_ID: z.string().optional(),\n GITHUB_CLIENT_SECRET: z.string().optional(),\n GOOGLE_CLIENT_ID: z.string().optional(),\n GOOGLE_CLIENT_SECRET: z.string().optional(),\n \n // App Configuration\n NEXT_PUBLIC_APP_URL: z.string().url(),\n NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),\n \n // Email (optional)\n SMTP_HOST: z.string().optional(),\n SMTP_PORT: z.string().optional(),\n SMTP_USER: z.string().optional(),\n SMTP_PASSWORD: z.string().optional(),\n})\n\nexport const env = envSchema.parse(process.env)\n\nexport type Env = z.infer<typeof envSchema>",
"scope": ["typescript"]
}
Dockerfile Rules
{
"name": "Next.js Dockerfile",
"description": "Generate optimized Dockerfile for Next.js apps",
"trigger": "dockerfile",
"expansion": "FROM node:18-alpine AS base\n\n# Install dependencies only when needed\nFROM base AS deps\nRUN apk add --no-cache libc6-compat\nWORKDIR /app\n\n# Install dependencies based on the preferred package manager\nCOPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./\nRUN \\\n if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\\n elif [ -f package-lock.json ]; then npm ci; \\\n elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \\\n else echo \"Lockfile not found.\" && exit 1; \\\n fi\n\n# Rebuild the source code only when needed\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\n\n# Generate Prisma client\nRUN npx prisma generate\n\n# Build the application\nRUN yarn build\n\n# Production image, copy all the files and run next\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV production\n\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\nCOPY --from=builder /app/public ./public\n\n# Set the correct permission for prerender cache\nRUN mkdir .next\nRUN chown nextjs:nodejs .next\n\n# Automatically leverage output traces to reduce image size\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./\nCOPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static\n\nUSER nextjs\n\nEXPOSE 3000\n\nENV PORT 3000\nENV HOSTNAME \"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]",
"scope": ["dockerfile"]
}
ESLint Configuration Rules
{
"name": "ESLint Config for Stack",
"description": "Generate comprehensive ESLint configuration",
"trigger": "eslintstack",
"expansion": "module.exports = {\n extends: [\n 'next/core-web-vitals',\n '@typescript-eslint/recommended',\n 'plugin:@typescript-eslint/recommended-requiring-type-checking',\n ],\n parser: '@typescript-eslint/parser',\n parserOptions: {\n project: './tsconfig.json',\n },\n plugins: ['@typescript-eslint'],\n rules: {\n // TypeScript specific rules\n '@typescript-eslint/no-unused-vars': 'error',\n '@typescript-eslint/no-explicit-any': 'warn',\n '@typescript-eslint/prefer-const': 'error',\n \n // React/Next.js rules\n 'react/prop-types': 'off',\n 'react/react-in-jsx-scope': 'off',\n 'react-hooks/exhaustive-deps': 'warn',\n \n // Import rules\n 'import/order': [\n 'error',\n {\n groups: [\n 'builtin',\n 'external',\n 'internal',\n 'parent',\n 'sibling',\n 'index',\n ],\n 'newlines-between': 'always',\n alphabetize: {\n order: 'asc',\n caseInsensitive: true,\n },\n },\n ],\n \n // General code quality\n 'no-console': 'warn',\n 'prefer-const': 'error',\n 'no-var': 'error',\n },\n overrides: [\n {\n files: ['**/*.test.ts', '**/*.test.tsx'],\n env: {\n jest: true,\n },\n },\n ],\n}",
"scope": ["javascript"]
}
š Recommended Project Structure
src/ āāā app/ # Next.js App Router ā āāā (auth)/ # Route groups ā āāā api/ # API routes ā āāā globals.css # Global styles āāā components/ # Reusable components ā āāā ui/ # Shadcn/UI components ā āāā forms/ # Form components āāā lib/ # Utility functions ā āāā auth.ts # BetterAuth configuration ā āāā db.ts # Database connection ā āāā utils.ts # General utilities āāā hooks/ # Custom React hooks āāā types/ # TypeScript type definitions āāā middleware.ts # Next.js middleware
Git Commit Message Rules
{
"name": "Conventional Commits",
"description": "Generate standardized commit messages",
"trigger": "gitcommit",
"patterns": {
"feat": "feat(${scope}): ${description}",
"fix": "fix(${scope}): ${description}",
"docs": "docs(${scope}): ${description}",
"style": "style(${scope}): ${description}",
"refactor": "refactor(${scope}): ${description}",
"test": "test(${scope}): ${description}",
"chore": "chore(${scope}): ${description}"
},
"scopes": [
"auth",
"ui",
"api",
"db",
"config",
"types",
"hooks",
"middleware"
]
}
ā ļø Common Auth Problems
- CORS Issues: Ensure your BetterAuth URL matches your app URL
- Session Persistence: Check cookie settings and domain configuration
- Provider Setup: Verify OAuth app configurations and redirect URLs
- Type Errors: Ensure proper TypeScript types for user sessions
Troubleshooting Steps (Bash)
# Check environment variables
echo $BETTER_AUTH_SECRET
echo $BETTER_AUTH_URL
# Verify database connection
npx prisma db pull
npx prisma generate
# Test authentication endpoints
curl -X POST http://localhost:3000/api/auth/session
# Check OAuth provider configuration
curl -X GET http://localhost:3000/api/auth/providers
š§ Common Component Problems
- Import Errors: Check if Shadcn components are properly installed
- Type Mismatches: Ensure props match component interfaces
- Styling Issues: Verify Tailwind CSS is properly configured
- State Management: Check for proper state initialization
Component Debugging Steps (TypeScript)
// Debug component props
interface DebugProps {
children: React.ReactNode
componentName: string
}
function DebugComponent({ children, componentName }: DebugProps) {
useEffect(() => {
console.log(`${componentName} rendered`, {
timestamp: new Date().toISOString(),
props: { children },
})
}, [children, componentName])
return <>{children}</>
}
š¾ Common Database Problems
- Migration Errors: Run
npx prisma migrate dev
to apply changes- Connection Issues: Verify DATABASE_URL in environment variables
- Type Generation: Run
npx prisma generate
after schema changes- Seed Data: Use
npx prisma db seed
to populate initial data
Bundle Analysis Rules (JavaScript)
// next.config.js
const nextConfig = {
experimental: {
optimizePackageImports: ['@radix-ui/react-icons'],
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.resolve.alias = {
...config.resolve.alias,
'@radix-ui/react-icons': '@radix-ui/react-icons/dist/index.js',
}
}
return config
},
}
module.exports = nextConfig
Optimized Query Patterns (TypeScript)
// Efficient data fetching with Prisma
async function getOptimizedUserData(userId: string) {
return await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
_count: {
select: {
posts: true,
comments: true,
},
},
},
})
}
// Parallel queries for better performance
async function getDashboardData(userId: string) {
const [user, posts, stats] = await Promise.all([
prisma.user.findUnique({ where: { id: userId } }),
prisma.post.findMany({
where: { authorId: userId },
take: 10,
orderBy: { createdAt: 'desc' },
}),
prisma.user.aggregate({
where: { id: userId },
_count: { posts: true },
}),
])
return { user, posts, stats }
}
š Staying Current
- Regular Updates: Keep dependencies up to date with tools like Renovate
- Feature Flags: Use feature flags for gradual rollouts of new features
- Testing Coverage: Maintain high test coverage for confident upgrades
- Migration Scripts: Create scripts for smooth database migrations
Scalable Architecture Patterns (TypeScript)
// Service layer pattern
class UserService {
constructor(private db: PrismaClient) {}
async createUser(data: CreateUserInput): Promise<User> {
return this.db.user.create({
data: {
...data,
id: generateId(),
createdAt: new Date(),
},
})
}
async getUserById(id: string): Promise<User | null> {
return this.db.user.findUnique({
where: { id },
include: {
profile: true,
settings: true,
},
})
}
}
// Repository pattern
interface UserRepository {
create(data: CreateUserInput): Promise<User>
findById(id: string): Promise<User | null>
findByEmail(email: string): Promise<User | null>
update(id: string, data: UpdateUserInput): Promise<User>
delete(id: string): Promise<void>
}
Through this comprehensive guide, you've learned how to leverage Cursor Rules to supercharge your development workflow with the Next.js + Shadcn/UI + TypeScript + BetterAuth stack. Here's your roadmap to mastery:
ā Key Takeaways
Mastering this tech stack with Cursor Rules transforms your development experience from manual repetition to intelligent automation. You gain not just speed, but consistency, quality, and the confidence to build scalable, maintainable applications. The combination of Next.js flexibility, Shadcn/UI beauty, TypeScript safety, and BetterAuth security creates a foundation for any modern web application.
This comprehensive guide will be continuously updated to reflect the latest features and best practices in the Next.js, Shadcn/UI, TypeScript, and BetterAuth ecosystem.
Expert developer passionate about modern web technologies and AI-assisted development.
Get the latest articles and tutorials delivered to your inbox.