Server Actions in Next.js 14 revolutionize how we handle forms by eliminating the need for API endpoints. Let's explore how to implement robust form handling with Server Actions, complete with validation, error handling, and file uploads.
Understanding Server Actions
Server Actions allow you to run server-side code directly from your React components. They're perfect for form submissions, data mutations, and any operation requiring server-side processing.
// app/actions.ts
'use server'
async function submitForm(formData: FormData) {
const name = formData.get('name')
const email = formData.get('email')
// Server-side validation
if (!name || !email) {
throw new Error('Name and email are required')
}
// Process the data
await saveToDatabase({ name, email })
}
Implementing Basic Form Handling
Let's create a simple contact form using Server Actions:
// app/contact/page.tsx
import { submitForm } from '../actions'
export default function ContactPage() {
return (
<form action={submitForm}>
<div className="mb-4">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
className="border p-2 w-full"
required
/>
</div>
<div className="mb-4">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
className="border p-2 w-full"
required
/>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2">
Submit
</button>
</form>
)
}
Advanced Error Handling
Server Actions can return data to the client, making it easy to handle errors and display feedback:
// app/actions.ts
'use server'
type FormResponse = {
success: boolean
message: string
}
export async function submitForm(formData: FormData): Promise<FormResponse> {
try {
// Validation
const name = formData.get('name')
const email = formData.get('email')
if (!name || !email) {
return {
success: false,
message: 'Please fill in all required fields'
}
}
// Process form data
await saveToDatabase({ name, email })
return {
success: true,
message: 'Form submitted successfully!'
}
} catch (error) {
return {
success: false,
message: 'An error occurred while submitting the form'
}
}
}
Handling File Uploads
Server Actions excel at handling file uploads without additional configuration:
// app/actions.ts
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File
if (!file) {
throw new Error('No file uploaded')
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const path = join('public', 'uploads', file.name)
await writeFile(path, buffer)
return { success: true, path: `/uploads/${file.name}` }
}
Optimistic Updates
Combine Server Actions with React's useOptimistic for smooth user experiences:
'use client'
import { useOptimistic } from 'react'
import { submitForm } from './actions'
export default function OptimisticForm() {
const [optimisticState, addOptimistic] = useOptimistic(
{ submitted: false },
(state, newState) => ({ ...state, ...newState })
)
async function handleSubmit(formData: FormData) {
addOptimistic({ submitted: true })
await submitForm(formData)
}
return (
<form action={handleSubmit}>
{/* Form fields */}
<button disabled={optimisticState.submitted}>
{optimisticState.submitted ? 'Submitting...' : 'Submit'}
</button>
</form>
)
}
Progressive Enhancement
Server Actions work without JavaScript, providing built-in progressive enhancement:
// app/components/ProgressiveForm.tsx
export default function ProgressiveForm() {
return (
<form action={submitForm}>
<input type="text" name="name" required />
<button type="submit">Submit</button>
{/* JavaScript-enhanced experience */}
<script>
{`
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault()
// Enhanced submission logic
})
`}
</script>
</form>
)
}
Best Practices and Tips
Type Safety: Use TypeScript for better type checking and autocompletion:
// app/lib/types.ts export type FormData = { name: string email: string }
// app/actions.ts 'use server'
import { FormData } from './lib/types'
export async function submitForm(data: FormData) { // Type-safe form handling }
Validation: Implement robust server-side validation:
import { z } from 'zod'
const schema = z.object({ name: z.string().min(2), email: z.string().email() })
export async function submitForm(formData: FormData) { const result = schema.safeParse({ name: formData.get('name'), email: formData.get('email') })
if (!result.success) { return { success: false, errors: result.error.flatten() } }
// Process valid data }
Rate Limiting: Protect your Server Actions from abuse:
import { rateLimit } from './lib/rate-limit'
export async function submitForm(formData: FormData) { const ip = headers().get('x-forwarded-for')
if (await rateLimit(ip)) { throw new Error('Too many requests') }
// Process form }
Conclusion
Server Actions in Next.js provide a powerful way to handle forms with minimal boilerplate while maintaining security and performance. They integrate seamlessly with React's latest features and support progressive enhancement out of the box.
Remember to:
- Implement proper validation on both client and server
- Handle errors gracefully
- Use TypeScript for better type safety
- Consider progressive enhancement
- Implement rate limiting for production applications
By following these practices, you'll create robust and user-friendly form handling solutions in your Next.js applications.