16 KiB
name, description, allowed-tools
| name | description | allowed-tools |
|---|---|---|
| nextjs-coding-standards | Next.js 16 coding standards including file naming conventions, API patterns, theming, styling guidelines, and directory structure. Use when writing or reviewing code. | Read, Grep, Glob |
Next.js 16 Coding Standards
Reference this skill when writing or reviewing code to ensure consistency with project conventions.
File Naming Conventions
Files and Directories
Use kebab-case for all file names:
✅ user-profile.tsx
✅ blog-post-card.tsx
✅ theme-toggle.tsx
❌ UserProfile.tsx
❌ blogPostCard.tsx
❌ ThemeToggle.tsx
Why kebab-case?
- Cross-platform compatibility (Windows vs Unix)
- URL-friendly (file names often map to routes)
- Easier to parse and read
- Industry standard for Next.js projects
Special Next.js Files:
page.tsx # Route pages
layout.tsx # Layout components
not-found.tsx # 404 pages
loading.tsx # Loading states
error.tsx # Error boundaries
route.ts # API route handlers
Component Names (Inside Files)
Use PascalCase for component names:
// File: user-profile.tsx
export function UserProfile() {
return <div>...</div>
}
// File: blog-post-card.tsx
export default function BlogPostCard() {
return <article>...</article>
}
Variables, Functions, Props
Use camelCase:
// Variables
const userSettings = {}
const isLoading = false
// Functions
function handleSubmit() {}
function formatDate(date: string) {}
// Props
interface ButtonProps {
onClick: () => void
isDisabled: boolean
className?: string
}
// Custom Hooks
function useTheme() {}
function useMarkdown() {}
Constants
Use SCREAMING_SNAKE_CASE:
const API_BASE_URL = "https://api.example.com"
const MAX_RETRIES = 3
const DEFAULT_LOCALE = "ro-RO"
Next.js 16 API Patterns
Route Handlers
Use Route Handlers (not legacy API Routes):
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
// Export named functions for HTTP methods
export async function GET(request: NextRequest) {
const posts = await getAllPosts()
return NextResponse.json({ posts })
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Process request
return NextResponse.json({ success: true }, { status: 201 })
}
Type-Safe Validation with Zod
Always validate input with Zod:
import { z } from 'zod'
import { NextRequest, NextResponse } from 'next/server'
const bodySchema = z.object({
title: z.string().min(1).max(200),
content: z.string(),
tags: z.array(z.string()).max(3),
})
export async function POST(request: NextRequest) {
const json = await request.json()
const parsed = bodySchema.safeParse(json)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error },
{ status: 400 }
)
}
// parsed.data is fully typed
const { title, content, tags } = parsed.data
// ... business logic
}
Key Points:
- Use
safeParse()instead ofparse()to avoid try/catch - Return structured error responses
- Use appropriate HTTP status codes
- Infer TypeScript types from Zod schemas
Error Handling
Return meaningful status codes:
200 // Success
201 // Created
400 // Bad Request (validation errors)
401 // Unauthorized
403 // Forbidden
404 // Not Found
500 // Internal Server Error
Structured error responses:
return NextResponse.json(
{
error: 'Resource not found',
code: 'NOT_FOUND',
timestamp: new Date().toISOString()
},
{ status: 404 }
)
Theming Patterns
next-themes Setup
Root Layout (Server Component):
// app/layout.tsx
import { ThemeProvider } from 'next-themes'
export default function RootLayout({ children }) {
return (
<html lang="ro" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
storageKey="blog-theme"
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Critical: Always add suppressHydrationWarning to <html> tag.
Theme Toggle Component
Avoid hydration mismatches with mounted state:
// components/theme-toggle.tsx
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// Prevent hydration mismatch
useEffect(() => setMounted(true), [])
if (!mounted) {
return <div className="w-9 h-9 animate-pulse" />
}
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '🌙' : '☀️'}
</button>
)
}
Pattern: Always check mounted state before rendering theme-dependent UI.
CSS Variables for Theme Tokens
Define in globals.css:
@layer base {
:root {
--bg-primary: 255 255 255;
--bg-secondary: 248 250 252;
--text-primary: 15 23 42;
--text-secondary: 51 65 85;
}
.dark {
--bg-primary: 24 24 27;
--bg-secondary: 15 23 42;
--text-primary: 241 245 249;
--text-secondary: 203 213 225;
}
}
Use in components:
<div className="bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))]">
Theme-aware component
</div>
Tailwind Configuration
Enable class-based dark mode:
// tailwind.config.js
module.exports = {
darkMode: 'class', // Required for next-themes
theme: {
extend: {
colors: {
'dark-primary': '#18181b',
'accent': {
DEFAULT: '#164e63',
hover: '#155e75',
},
},
},
},
}
Use dark: variant:
<div className="bg-slate-100 dark:bg-zinc-900 text-slate-900 dark:text-slate-100">
Automatically switches based on theme
</div>
Styling Guidelines (Tailwind CSS)
Utility-First Philosophy
Prefer inline utilities over custom CSS:
// ✅ Good: Inline utilities
<article className="border-4 border-slate-800 p-6 bg-zinc-900 hover:border-cyan-900">
<h2 className="text-2xl font-bold uppercase tracking-wider">Title</h2>
</article>
// ❌ Avoid: @apply (increases bundle size)
/* styles.css */
.card {
@apply border-4 border-slate-800 p-6 bg-zinc-900;
}
Exception: Only use @apply for truly global base styles in globals.css.
Component Extraction
Extract to React components when reused:
// ✅ Extract repeated patterns to components
export function Card({ children, className = "" }) {
return (
<div className={`border-4 border-slate-800 p-6 bg-zinc-900 ${className}`}>
{children}
</div>
)
}
// Usage
<Card className="hover:border-cyan-900">
<h2>Title</h2>
</Card>
Don't extract: One-off components or single-use patterns.
Class Organization
Group utilities logically:
// Layout → Spacing → Colors → Typography → Effects
<div className="
flex flex-col // Layout
gap-4 p-6 // Spacing
bg-zinc-900 // Colors
border-4 border-slate-800
text-slate-100
font-mono text-sm // Typography
uppercase tracking-wider
hover:border-cyan-900 // Effects
transition-colors
">
Tip: Use Prettier plugin for automatic Tailwind class sorting.
Responsive Design
Mobile-first approach:
// Base classes = mobile, add breakpoints for larger screens
<div className="
flex-col // Mobile: vertical stack
md:flex-row // Tablet+: horizontal layout
lg:gap-8 // Desktop: more spacing
">
Standard breakpoints:
sm: 640px // Small tablets
md: 768px // Tablets
lg: 1024px // Laptops
xl: 1280px // Desktops
2xl: 1536px // Large screens
Conditional Styling
For complex conditions, use variants:
const cardVariants = {
default: "border-slate-800 bg-zinc-900",
highlighted: "border-cyan-600 bg-cyan-950",
error: "border-red-600 bg-red-950",
}
<Card className={cardVariants[variant]} />
Directory Structure Standards
Project Organization
app/
├── (auth)/ # Route groups (no URL segment)
│ ├── login/
│ └── register/
├── api/ # API routes
│ └── posts/
│ └── route.ts
├── blog/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
├── @breadcrumbs/ # Parallel routes
│ └── default.tsx
├── layout.tsx # Root layout
├── globals.css
└── page.tsx
components/
├── blog/ # Domain-specific components
│ ├── post-card.tsx
│ └── markdown-renderer.tsx
├── layout/ # Layout components
│ ├── header.tsx
│ └── footer.tsx
└── ui/ # Reusable UI primitives
├── button.tsx
└── card.tsx
lib/
├── api/ # API clients, fetch wrappers
│ └── client.ts
├── types/ # TypeScript type definitions
│ └── post.ts
├── markdown.ts # Business logic modules
├── seo.ts
└── utils.ts # Pure utility functions
public/
├── blog/ # Blog-specific assets
│ └── images/
└── icons/
content/ # Content files (outside app/)
└── blog/
└── posts.md
lib/ Organization
Modules in lib/:
- Substantial business logic (markdown.ts, seo.ts)
- API clients and data fetching
- Database connections
- Authentication logic
Utils in lib/utils.ts:
- Pure helper functions
- Formatters (formatDate, formatCurrency)
- Validators (isEmail, isValidUrl)
- String manipulations
Types in lib/types/:
- Shared TypeScript interfaces
- API response types
- Domain models
- Colocated with their modules when possible
Component Organization
By domain/feature:
components/
├── blog/ # Blog-specific
│ ├── post-card.tsx
│ ├── post-list.tsx
│ └── markdown-renderer.tsx
├── auth/ # Auth-specific
│ ├── login-form.tsx
│ └── signup-form.tsx
└── ui/ # Reusable primitives
├── button.tsx
├── card.tsx
└── input.tsx
Not by type:
❌ Don't organize like this:
components/
├── forms/
├── buttons/
├── cards/
└── modals/
Public Assets
Organize by feature:
public/
├── blog/
│ ├── images/
│ └── thumbnails/
├── icons/
│ ├── social/
│ └── ui/
└── fonts/
Naming conventions:
- Use descriptive names:
hero-background.jpgnotimg1.jpg - Use kebab-case:
user-avatar.png - Include dimensions for images:
logo-512x512.png
TypeScript Best Practices
Type Safety
Avoid any:
// ❌ Bad
function processData(data: any) { }
// ✅ Good
function processData(data: unknown) {
if (typeof data === 'string') {
// TypeScript knows data is string here
}
}
// ✅ Better: Use specific types
interface PostData {
title: string
content: string
}
function processData(data: PostData) { }
Infer Types from Zod
Don't duplicate types:
import { z } from 'zod'
// Define schema once
const postSchema = z.object({
title: z.string(),
content: z.string(),
tags: z.array(z.string()),
})
// Infer TypeScript type
type Post = z.infer<typeof postSchema>
// Now you have both runtime validation and compile-time types
Type Imports
Use type imports for types only:
// ✅ Good: Explicit type import
import type { Metadata } from 'next'
import type { Post } from '@/lib/types/post'
// ✅ Mixed: Regular and type imports
import { getAllPosts } from '@/lib/markdown'
import type { Post } from '@/lib/types/post'
Next.js 16 Specific Patterns
Async Server Components
Fetch data directly in components:
// app/blog/page.tsx
export default async function BlogPage() {
// Server-side data fetching (no useEffect needed)
const posts = await getAllPosts()
return (
<div>
{posts.map(post => (
<PostCard key={post.slug} post={post} />
))}
</div>
)
}
Static Generation
Use generateStaticParams for dynamic routes:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({
slug: post.slug.split('/'), // For catch-all routes
}))
}
export async function generateMetadata({ params }) {
const post = getPostBySlug(params.slug.join('/'))
return {
title: post.frontmatter.title,
description: post.frontmatter.description,
}
}
export default async function PostPage({ params }) {
const post = getPostBySlug(params.slug.join('/'))
return <article>{/* render post */}</article>
}
Client Components
Minimize 'use client' usage:
// ❌ Unnecessary client component
'use client'
export function StaticCard({ title }) {
return <div>{title}</div>
}
// ✅ Keep as server component (default)
export function StaticCard({ title }) {
return <div>{title}</div>
}
// ✅ Only use 'use client' when necessary
'use client'
import { useState } from 'react'
export function InteractiveCard({ title }) {
const [isOpen, setIsOpen] = useState(false)
return (
<div onClick={() => setIsOpen(!isOpen)}>
{title}
</div>
)
}
When to use 'use client':
- Using React hooks (useState, useEffect, etc.)
- Using event handlers (onClick, onChange, etc.)
- Using browser APIs (window, localStorage, etc.)
- Using context consumers
Parallel Routes
Use for layout composition:
// app/layout.tsx
export default function RootLayout({
children,
breadcrumbs, // From @breadcrumbs parallel route
}: {
children: React.ReactNode
breadcrumbs: React.ReactNode
}) {
return (
<>
{breadcrumbs}
<main>{children}</main>
</>
)
}
Common Pitfalls
1. Hydration Mismatches
Problem: Theme-dependent content renders differently on server vs client.
Solution: Use mounted state pattern (see Theming section).
2. Image Paths
Problem: Incorrect public asset paths.
// ❌ Wrong
<Image src="blog/image.jpg" />
// ✅ Correct
<Image src="/blog/image.jpg" /> // Leading slash
3. Dynamic Route Params
Problem: Forgetting slug should be an array for catch-all routes.
// app/blog/[...slug]/page.tsx
export async function generateStaticParams() {
// ❌ Wrong
return posts.map(post => ({ slug: post.slug }))
// ✅ Correct
return posts.map(post => ({ slug: post.slug.split('/') }))
}
4. Over-using Client Components
Problem: Adding 'use client' unnecessarily.
Solution: Keep components as Server Components by default. Only add 'use client' when you need hooks, events, or browser APIs.
5. Date Formats
Problem: Inconsistent date formatting.
Solution: Use consistent ISO format (YYYY-MM-DD) in data, format for display:
// In frontmatter
date: "2025-01-15"
// For display
formatDate(post.frontmatter.date) // "15 ianuarie 2025" (Romanian)
Reference
For project-specific architecture and design philosophy, see CLAUDE.md at the root of this repository.
This skill focuses on coding conventions and standards. For architecture patterns, markdown processing, and industrial design aesthetic guidelines, refer to the main documentation.