/**
* Next.js 16 - Server Actions for Form Handling
*
* This template shows comprehensive Server Actions patterns including:
* - Basic form handling
* - Validation with Zod
* - Loading states
* - Error handling
* - Optimistic updates
* - File uploads
*/
'use server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
// ============================================================================
// Example 1: Basic Server Action
// ============================================================================
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Save to database
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
})
// Revalidate cache
revalidateTag('posts', 'max')
// Redirect to posts list
redirect('/posts')
}
// Basic form component
export function CreatePostForm() {
return (
)
}
// ============================================================================
// Example 2: Server Action with Validation
// ============================================================================
const PostSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().min(10, 'Content must be at least 10 characters'),
tags: z.array(z.string()).min(1, 'At least one tag is required'),
})
type ActionState = {
errors?: {
title?: string[]
content?: string[]
tags?: string[]
_form?: string[]
}
success?: boolean
}
export async function createPostWithValidation(
prevState: ActionState,
formData: FormData
): Promise {
// Parse form data
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
tags: formData.get('tags')?.toString().split(',') || [],
}
// Validate
const parsed = PostSchema.safeParse(rawData)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Save to database
try {
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed.data),
})
revalidateTag('posts', 'max')
return { success: true }
} catch (error) {
return {
errors: {
_form: ['Failed to create post. Please try again.'],
},
}
}
}
// Form with validation errors
'use client'
import { useFormState, useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
)
}
export function ValidatedPostForm() {
const [state, formAction] = useFormState(createPostWithValidation, {})
return (
)
}
// ============================================================================
// Example 3: Server Action with Optimistic Updates
// ============================================================================
'use server'
import { updateTag } from 'next/cache'
export async function likePost(postId: string) {
await fetch(`https://api.example.com/posts/${postId}/like`, {
method: 'POST',
})
// Use updateTag for immediate refresh (read-your-writes)
updateTag('posts')
}
// Client component with optimistic updates
'use client'
import { useOptimistic } from 'react'
import { likePost } from './actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
async function handleLike() {
// Update UI immediately
addOptimisticLike(1)
// Sync with server
await likePost(postId)
}
return (
)
}
// ============================================================================
// Example 4: Server Action with File Upload
// ============================================================================
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadImage(formData: FormData) {
const file = formData.get('image') as File
if (!file) {
return { error: 'No file provided' }
}
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!validTypes.includes(file.type)) {
return { error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024 // 5MB
if (file.size > maxSize) {
return { error: 'File too large. Maximum size is 5MB.' }
}
// Save file
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public', 'uploads', filename)
await writeFile(path, buffer)
return {
success: true,
url: `/uploads/${filename}`,
}
}
// File upload form
'use client'
import { useState } from 'react'
import { uploadImage } from './actions'
export function ImageUploadForm() {
const [preview, setPreview] = useState(null)
const [error, setError] = useState(null)
async function handleSubmit(formData: FormData) {
const result = await uploadImage(formData)
if (result.error) {
setError(result.error)
} else if (result.url) {
setPreview(result.url)
setError(null)
}
}
return (
)
}
// ============================================================================
// Example 5: Server Action with Progressive Enhancement
// ============================================================================
'use server'
export async function subscribe(formData: FormData) {
const email = formData.get('email') as string
await fetch('https://api.example.com/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
return { success: true, message: 'Subscribed successfully!' }
}
// Form works without JavaScript (progressive enhancement)
export function SubscribeForm() {
return (
)
}
// Enhanced with JavaScript
'use client'
import { useFormState } from 'react-dom'
export function EnhancedSubscribeForm() {
const [state, formAction] = useFormState(subscribe, null)
return (
)
}
// ============================================================================
// Example 6: Server Action with Multi-Step Form
// ============================================================================
'use server'
type Step1Data = { name: string; email: string }
type Step2Data = { address: string; city: string }
type FormData = Step1Data & Step2Data
export async function submitMultiStepForm(data: FormData) {
// Validate all data
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
address: z.string().min(5),
city: z.string().min(2),
})
const parsed = schema.safeParse(data)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Save to database
await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed.data),
})
return { success: true }
}
// Multi-step form component
'use client'
import { useState } from 'react'
export function MultiStepForm() {
const [step, setStep] = useState(1)
const [formData, setFormData] = useState>({})
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (step === 1) {
const form = e.currentTarget
setFormData({
...formData,
name: (form.elements.namedItem('name') as HTMLInputElement).value,
email: (form.elements.namedItem('email') as HTMLInputElement).value,
})
setStep(2)
} else {
const form = e.currentTarget
const finalData = {
...formData,
address: (form.elements.namedItem('address') as HTMLInputElement).value,
city: (form.elements.namedItem('city') as HTMLInputElement).value,
} as FormData
const result = await submitMultiStepForm(finalData)
if (result.success) {
alert('Form submitted successfully!')
}
}
}
return (
)
}
// ============================================================================
// Example 7: Server Action with Rate Limiting
// ============================================================================
'use server'
import { headers } from 'next/headers'
const rateLimitMap = new Map()
export async function submitContactForm(formData: FormData) {
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') || 'unknown'
// Check rate limit (5 requests per hour)
const now = Date.now()
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 5) {
return { error: 'Too many requests. Please try again later.' }
}
rateLimit.count++
} else {
// Reset rate limit
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 }) // 1 hour
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 })
}
// Process form
const name = formData.get('name') as string
const message = formData.get('message') as string
await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, message }),
})
return { success: true }
}
/**
* Summary:
*
* Server Actions patterns:
* 1. ✅ Basic form handling with formData
* 2. ✅ Validation with Zod (safeParse)
* 3. ✅ Loading states with useFormStatus
* 4. ✅ Error handling with useFormState
* 5. ✅ Optimistic updates with useOptimistic
* 6. ✅ File uploads
* 7. ✅ Progressive enhancement (works without JS)
* 8. ✅ Multi-step forms
* 9. ✅ Rate limiting
*
* Best practices:
* - Always validate on server (never trust client input)
* - Use revalidateTag() for background revalidation
* - Use updateTag() for immediate refresh (forms, settings)
* - Return errors instead of throwing (better UX)
* - Use TypeScript for type safety
* - Add rate limiting for public forms
*/