| name | frontend-dev-guidelines |
| description | Frontend development guidelines for Next.js 15 applications with React 19, TypeScript, Shadcn/ui, and Tailwind CSS. Modern patterns including Server Components, Client Components, App Router, file organization, Shadcn/ui components, performance optimization, and TypeScript best practices. Use when creating components, pages, features, fetching data, styling, routing, or working with frontend code. |
Frontend Development Guidelines
Purpose
Comprehensive guide for modern Next.js 15 development with React 19, emphasizing Server Components, Client Components, App Router patterns, Shadcn/ui components, proper file organization, and performance optimization.
When to Use This Skill
- Creating new components or pages
- Building new features
- Fetching data (Server Components, Server Actions)
- Setting up routing with Next.js App Router
- Styling components with Tailwind CSS and Shadcn/ui
- Performance optimization
- Organizing frontend code
- TypeScript best practices
Quick Start
New Component Checklist
Creating a component? Follow this checklist:
- Determine Server vs Client Component (default: Server Component)
- Add
"use client"directive only if needed (interactivity, hooks, browser APIs) - Use TypeScript with explicit prop types
- Import Shadcn/ui components from
@/components/ui - Use Tailwind CSS classes for styling
- Import aliases:
@/components,@/lib,@/hooks - Use
cn()utility for conditional classes - Default export at bottom
- Use Server Components for data fetching when possible
New Page Checklist
Creating a page? Set up this structure:
- Create
app/{route-name}/page.tsxfor route - Use Server Component by default
- Fetch data directly in Server Component
- Create
components/directory for page-specific components - Use
loading.tsxfor loading states - Use
error.tsxfor error boundaries - Export metadata for SEO
Import Aliases Quick Reference
| Alias | Resolves To | Example |
|---|---|---|
@/ |
Project root | import { cn } from '@/lib/utils' |
@/components |
components/ |
import { Button } from '@/components/ui/button' |
@/lib |
lib/ |
import { cn } from '@/lib/utils' |
@/hooks |
hooks/ |
import { useMobile } from '@/hooks/use-mobile' |
@/app |
app/ |
import { Metadata } from 'next' |
Defined in: tsconfig.json paths configuration
Common Imports Cheatsheet
// Next.js
import { Metadata } from 'next'
import { Suspense } from 'react'
import { notFound, redirect } from 'next/navigation'
// React (Client Components only)
;('use client')
import { useState, useCallback, useMemo } from 'react'
// Shadcn/ui Components
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
// Utilities
import { cn } from '@/lib/utils'
// Hooks (Client Components only)
import { useMobile } from '@/hooks/use-mobile'
// Types
import type { ComponentProps } from 'react'
Topic Guides
🎨 Component Patterns
Server Components vs Client Components:
- Server Components (default): No
"use client", can fetch data directly, smaller bundle - Client Components: Add
"use client"for interactivity, hooks, browser APIs
Key Concepts:
- Default to Server Components
- Only use Client Components when necessary
- Use Shadcn/ui components (already Client Components)
- Component structure: Props → Data Fetching → Render → Export
Example Server Component:
// app/features/posts/components/PostList.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
interface PostListProps {
posts: Post[]
}
export function PostList({ posts }: PostListProps) {
return (
<div className='grid gap-4'>
{posts.map((post) => (
<Card key={post.id}>
<CardHeader>
<CardTitle>{post.title}</CardTitle>
</CardHeader>
<CardContent>{post.content}</CardContent>
</Card>
))}
</div>
)
}
Example Client Component:
// app/features/posts/components/PostForm.tsx
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
export function PostForm() {
const [title, setTitle] = useState('')
return (
<form>
<Input value={title} onChange={(e) => setTitle(e.target.value)} />
<Button type='submit'>Submit</Button>
</form>
)
}
📊 Data Fetching
PRIMARY PATTERN: Server Components
- Fetch data directly in Server Components
- Use
async/awaitin Server Components - No need for
useEffector data fetching libraries - Automatic request deduplication
Server Actions:
- Use for mutations (forms, updates)
- Create
app/actions/directory - Mark with
"use server"directive
Example Server Component with Data Fetching:
// app/posts/page.tsx
import { PostList } from '@/components/PostList'
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // or 'force-cache', 'revalidate'
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return <PostList posts={posts} />
}
Example Server Action:
// app/actions/posts.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
// ... validation and creation logic
redirect('/posts')
}
📁 File Organization
App Router Structure:
app/
(routes)/
page.tsx # Route page
layout.tsx # Route layout
loading.tsx # Loading UI
error.tsx # Error UI
components/ # Shared components
ui/ # Shadcn/ui components
features/ # Feature-specific code
posts/
components/ # Feature components
actions/ # Server Actions
types/ # TypeScript types
lib/
utils.ts # Utilities (cn, etc.)
hooks/
use-mobile.ts # Custom hooks (Client only)
Feature Organization:
app/features/{feature}/: Feature-specific pages/routescomponents/: Truly reusable componentscomponents/ui/: Shadcn/ui components (don't modify directly)
🎨 Styling
Tailwind CSS + Shadcn/ui:
- Use Tailwind utility classes
- Use
cn()utility for conditional classes - Shadcn/ui components use CSS variables for theming
- Customize theme in
app/globals.css
Styling Patterns:
import { cn } from '@/lib/utils'
interface ButtonProps {
variant?: 'primary' | 'secondary'
className?: string
}
export function Button({ variant = 'primary', className }: ButtonProps) {
return (
<button
className={cn(
'rounded-md px-4 py-2',
variant === 'primary' && 'bg-primary text-primary-foreground',
variant === 'secondary' && 'bg-secondary text-secondary-foreground',
className,
)}
>
Click me
</button>
)
}
Shadcn/ui Components:
- Import from
@/components/ui/{component-name} - Components are already styled and accessible
- Customize via
classNameprop or CSS variables
🛣️ Routing
Next.js App Router - File-Based:
- Directory:
app/{route-name}/page.tsx - Nested routes:
app/{parent}/{child}/page.tsx - Dynamic routes:
app/posts/[id]/page.tsx - Route groups:
app/(marketing)/about/page.tsx
Example Route:
// app/posts/page.tsx
import { Metadata } from 'next'
import { PostList } from '@/components/PostList'
export const metadata: Metadata = {
title: 'Posts',
description: 'List of all posts',
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div className='container mx-auto py-8'>
<h1 className='text-3xl font-bold mb-6'>Posts</h1>
<PostList posts={posts} />
</div>
)
}
Dynamic Route:
// app/posts/[id]/page.tsx
interface PostPageProps {
params: Promise<{ id: string }>
}
export default async function PostPage({ params }: PostPageProps) {
const { id } = await params
const post = await getPost(id)
if (!post) {
notFound()
}
return <PostDetail post={post} />
}
⏳ Loading & Error States
Loading States:
- Create
loading.tsxin route directory - Automatically wraps page in Suspense
- Use for route-level loading
Error Boundaries:
- Create
error.tsxin route directory - Automatically catches errors in route
- Can reset error state
Example Loading UI:
// app/posts/loading.tsx
export default function Loading() {
return (
<div className='flex items-center justify-center min-h-screen'>
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary' />
</div>
)
}
Example Error UI:
// app/posts/error.tsx
'use client'
import { useEffect } from 'react'
import { Button } from '@/components/ui/button'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className='flex flex-col items-center justify-center min-h-screen'>
<h2 className='text-2xl font-bold mb-4'>Something went wrong!</h2>
<Button onClick={reset}>Try again</Button>
</div>
)
}
⚡ Performance
Optimization Patterns:
- Use Server Components (smaller bundle)
- Use
next/imagefor images - Use
next/fontfor fonts - Lazy load Client Components when possible
- Use
useMemoanduseCallbackin Client Components - Stream data with Suspense boundaries
Image Optimization:
import Image from 'next/image'
export function Avatar({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={40}
height={40}
className='rounded-full'
/>
)
}
Streaming with Suspense:
import { Suspense } from 'react'
import { PostList } from '@/components/PostList'
import { Loading } from '@/components/Loading'
export default function Page() {
return (
<div>
<Suspense fallback={<Loading />}>
<PostList />
</Suspense>
</div>
)
}
📘 TypeScript
Standards:
- Strict mode enabled
- No
anytype - Explicit return types on functions
- Type imports:
import type { Post } from '@/types/post' - Component prop interfaces with JSDoc
Example:
import type { ComponentProps } from 'react'
import { Button } from '@/components/ui/button'
/**
* Custom button component with loading state
*/
interface CustomButtonProps extends ComponentProps<typeof Button> {
isLoading?: boolean
}
export function CustomButton({
isLoading,
children,
...props
}: CustomButtonProps) {
return (
<Button disabled={isLoading} {...props}>
{isLoading ? 'Loading...' : children}
</Button>
)
}
🔧 Common Patterns
Form Handling:
- Use Server Actions for form submissions
- Use
react-hook-formwithzodfor validation (Client Components) - Use Shadcn/ui Form components
Example Form with Server Action:
// app/actions/posts.ts
'use server'
import { z } from 'zod'
const createPostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
})
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
}
const validated = createPostSchema.parse(rawData)
// ... create post logic
redirect('/posts')
}
Metadata:
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Posts',
description: 'List of all posts',
openGraph: {
title: 'Posts',
description: 'List of all posts',
},
}
Core Principles
- Server Components First: Default to Server Components, use Client Components only when needed
- App Router Structure: Use file-based routing with
app/directory - Shadcn/ui Components: Use pre-built accessible components
- Tailwind CSS: Utility-first styling with
cn()helper - TypeScript Strict: No
any, explicit types - Performance: Use Server Components, optimize images, lazy load when needed
- File Organization: Features in
app/features/, shared incomponents/ - Import Aliases: Use
@/prefix for clean imports
Quick Reference: File Structure
app/
layout.tsx # Root layout
page.tsx # Home page
globals.css # Global styles
(routes)/
posts/
page.tsx # Posts list page
[id]/
page.tsx # Post detail page
loading.tsx # Loading UI
error.tsx # Error UI
features/
posts/
components/
PostList.tsx # Feature components
actions/
posts.ts # Server Actions
components/
ui/ # Shadcn/ui components
button.tsx
card.tsx
lib/
utils.ts # Utilities (cn, etc.)
hooks/
use-mobile.ts # Custom hooks
Modern Component Template (Quick Copy)
Server Component:
// app/components/PostCard.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import type { Post } from '@/types/post'
interface PostCardProps {
post: Post
}
export function PostCard({ post }: PostCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>{post.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{post.content}</p>
</CardContent>
</Card>
)
}
Client Component:
// app/components/PostForm.tsx
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { createPost } from '@/app/actions/posts'
import { cn } from '@/lib/utils'
export function PostForm({ className }: { className?: string }) {
const [isLoading, setIsLoading] = useState(false)
async function handleSubmit(formData: FormData) {
setIsLoading(true)
await createPost(formData)
setIsLoading(false)
}
return (
<form action={handleSubmit} className={cn('space-y-4', className)}>
<Input name='title' placeholder='Post title' required />
<Input name='content' placeholder='Post content' required />
<Button type='submit' disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Post'}
</Button>
</form>
)
}
Related Skills
- backend-dev-guidelines: Backend API patterns that frontend consumes
Skill Status: Optimized for Next.js 15 with App Router, Server Components, and Shadcn/ui