SupaLaunch logo
SUPALAUNCH
ShowcaseDocs
SupaLaunch
HomeDemoDocumentationBlogShowcaseServices and FrameworksTerms of ServicePrivacy policy
Tools
DB Schema GeneratorStartup Idea Generator
Other Projects
Syntha AICreateChatsCron Expression GeneratorAI Prompts Generator
Contacts
Created with ❤️ by Denisdenis@supalaunch.com
  • Showcase
  • Docs
Blog

Mastering Next.js Server Actions: A Complete Guide to Form Handling and Data Mutations

1 Dec 2024
By Denis
nextjsformsserver-actionsreact

Launch your startup with our Supabase + NextJS Starter Kit

SupaLaunch is a SaaS boilerplate built using Supabase and Next.js. It includes authentication, Stripe payments, Postgres database, 20+ TailwindCSS themes, emails, OpenAI API streaming, file storage and more.

  • Save weeks of your time: No need to setup authentication, payments, emails, file storage, etc.
  • Focus on what matters: Spend your time building your product, not boilerplate code.
Get started with SupaLaunch

Mastering Next.js Server Actions

Next.js Server Actions provide a powerful way to handle form submissions and data mutations without creating separate API endpoints. In this comprehensive guide, we'll explore how to implement Server Actions effectively, covering everything from basic form handling to advanced security considerations.

What are Server Actions?

Server Actions are asynchronous functions that execute on the server side. They're designed to handle data mutations and form submissions in Next.js applications, offering several key benefits:

  • No need for separate API endpoints
  • Built-in security features
  • Progressive enhancement support
  • Seamless integration with Next.js caching
  • Type safety with TypeScript

Basic Implementation

Let's start with a simple example of how to create and use a Server Action:

'use server'

export async function createItem(formData: FormData) {
  const name = formData.get('name')
  const description = formData.get('description')
  
  // Perform database operation
  await db.items.create({
    data: {
      name,
      description
    }
  })
}

You can use this action in both Server and Client Components:

'use client'

import { createItem } from '@/app/actions'

export function CreateForm() {
  return (
    <form action={createItem}>
      <input name="name" type="text" required />
      <textarea name="description" required />
      <button type="submit">Create Item</button>
    </form>
  )
}

Server Actions in Forms

Server Actions integrate seamlessly with HTML forms. Here are the key patterns:

1. Form Action Binding

// Direct form action
<form action={createItem}>

// With additional arguments using bind
const createItemWithId = createItem.bind(null, itemId)
<form action={createItemWithId}>

2. Progressive Enhancement

Server Actions support progressive enhancement out of the box. Forms will work even if JavaScript is disabled:

export default function ProgressiveForm() {
  async function handleSubmit(formData: FormData) {
    'use server'
    // This works with JS disabled
    await createItem(formData)
  }

  return <form action={handleSubmit}>...</form>
}

Data Validation and Type Safety

It's crucial to validate form data before processing it. Here's how to use Zod for type validation:

'use server'

import { z } from 'zod'

const ItemSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().min(1),
  price: z.coerce.number().positive()
})

export async function createItem(formData: FormData) {
  const validatedFields = ItemSchema.safeParse({
    name: formData.get('name'),
    description: formData.get('description'),
    price: formData.get('price')
  })

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors
    }
  }

  // Process validated data
  const { name, description, price } = validatedFields.data
}

Handling Server Action States

React provides hooks to handle loading and error states:

'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button disabled={pending} type="submit">
      {pending ? 'Creating...' : 'Create Item'}
    </button>
  )
}

Optimistic Updates

Implement optimistic updates using the useOptimistic hook:

'use client'

import { useOptimistic } from 'react'
import { createItem } from '@/app/actions'

export function ItemsList({ items }) {
  const [optimisticItems, addOptimisticItem] = useOptimistic(
    items,
    (state, newItem) => [...state, newItem]
  )

  async function handleCreate(formData: FormData) {
    const newItem = {
      name: formData.get('name'),
      description: formData.get('description')
    }
    
    addOptimisticItem(newItem)
    await createItem(formData)
  }

  return (
    <div>
      {optimisticItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      <form action={handleCreate}>...</form>
    </div>
  )
}

Cache Revalidation

Next.js provides APIs to revalidate the cache after mutations:

'use server'

import { revalidatePath, revalidateTag } from 'next/cache'

export async function updateItem(id: string, formData: FormData) {
  await db.items.update({
    where: { id },
    data: {
      name: formData.get('name')
    }
  })

  // Revalidate specific path
  revalidatePath('/items')
  
  // Or revalidate by tag
  revalidateTag('items')
}

Security Considerations

Server Actions include several security features:

  1. Encrypted Action IDs: Next.js creates encrypted, non-deterministic IDs for Server Actions.

  2. CSRF Protection: Server Actions only accept POST requests and validate Origin headers.

  3. Allowed Origins: Configure trusted origins in your Next.js config:

/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-trusted-domain.com'],
      bodySizeLimit: '2mb'
    }
  }
}
  1. Size Limits: Configure request size limits to prevent DOS attacks:
module.exports = {
  experimental: {
    serverActions: {
      bodySizeLimit: '1mb' // Default
    }
  }
}

Best Practices

1. Structured Error Handling

Always implement comprehensive error handling with specific error types and messages:

'use server'

type ActionResponse = {
  success: boolean;
  data?: any;
  error?: {
    message: string;
    code?: string;
    field?: string;
  };
}

export async function createItem(formData: FormData): Promise<ActionResponse> {
  try {
    // Validate input
    const name = formData.get('name')
    if (!name) {
      return {
        success: false,
        error: {
          message: 'Name is required',
          field: 'name'
        }
      }
    }

    // Attempt to create item
    const item = await db.items.create({
      data: { name }
    })

    return {
      success: true,
      data: item
    }

  } catch (error) {
    console.error('Create item error:', error)
    return {
      success: false,
      error: {
        message: 'Failed to create item. Please try again.',
        code: 'CREATE_FAILED'
      }
    }
  }
}

2. Input Validation Strategies

Implement multiple layers of validation:

'use server'

import { z } from 'zod'

// 1. Type validation with Zod
const UserSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18),
  preferences: z.array(z.string())
})

// 2. Business logic validation
function validateBusinessRules(data: z.infer<typeof UserSchema>) {
  const errors = []

  if (data.preferences.length > 5) {
    errors.push('Maximum 5 preferences allowed')
  }

  // Add more business rules

  return errors
}

export async function createUser(formData: FormData) {
  // 1. Parse and validate types
  const validatedFields = UserSchema.safeParse({
    email: formData.get('email'),
    age: Number(formData.get('age')),
    preferences: formData.getAll('preferences')
  })

  if (!validatedFields.success) {
    return {
      success: false,
      errors: validatedFields.error.flatten().fieldErrors
    }
  }

  // 2. Validate business rules
  const businessErrors = validateBusinessRules(validatedFields.data)
  if (businessErrors.length > 0) {
    return {
      success: false,
      errors: businessErrors
    }
  }

  // 3. Proceed with creation
  try {
    // Create user
  } catch (error) {
    // Handle errors
  }
}

3. Efficient Cache Management

Implement strategic cache invalidation:

'use server'

import { revalidatePath, revalidateTag } from 'next/cache'

export async function updateUserProfile(userId: string, formData: FormData) {
  try {
    await db.users.update({
      where: { id: userId },
      data: {
        name: formData.get('name'),
        bio: formData.get('bio')
      }
    })

    // Revalidate specific paths
    revalidatePath(`/users/${userId}`) // User profile page
    revalidatePath('/users') // Users list page

    // Revalidate related tags
    revalidateTag(`user-${userId}`)
    revalidateTag('users-list')

    return { success: true }
  } catch (error) {
    return { success: false, error }
  }
}

4. Progressive Enhancement Implementation

Ensure forms work without JavaScript while providing enhanced functionality when available:

'use client'

import { useFormState } from 'react-dom'
import { createItem } from '@/app/actions'

export function EnhancedForm() {
  const [state, formAction] = useFormState(createItem, {
    success: false,
    error: null
  })

  return (
    <form action={formAction}>
      {/* Basic form that works without JS */}
      <input type="text" name="name" required />

      {/* Enhanced features when JS is available */}
      {state.error && (
        <div className="error-message">
          {state.error.message}
        </div>
      )}

      <SubmitButton />
    </form>
  )
}

5. Security Best Practices

Implement comprehensive security measures:

'use server'

import { headers } from 'next/headers'
import { validateCSRFToken } from '@/lib/security'

export async function secureAction(formData: FormData) {
  // 1. Validate CSRF token
  const headersList = headers()
  const token = headersList.get('x-csrf-token')

  if (!await validateCSRFToken(token)) {
    throw new Error('Invalid CSRF token')
  }

  // 2. Rate limiting
  const ip = headersList.get('x-forwarded-for')
  if (await isRateLimited(ip)) {
    throw new Error('Too many requests')
  }

  // 3. Input sanitization
  const sanitizedData = sanitizeInput(formData)

  // 4. Authorization check
  const user = await getCurrentUser()
  if (!user.hasPermission('create:items')) {
    throw new Error('Unauthorized')
  }

  // Proceed with action
  // ...
}

6. Performance Optimization

Optimize server actions for better performance:

'use server'

export async function optimizedAction(formData: FormData) {
  // 1. Batch database operations
  const batch = await db.batch([
    ['set', 'key1', formData.get('value1')],
    ['set', 'key2', formData.get('value2')]
  ])

  // 2. Parallel processing when possible
  const [result1, result2] = await Promise.all([
    processData1(formData),
    processData2(formData)
  ])

  // 3. Efficient cache invalidation
  revalidateTag('affected-data')
}

7. Testing Strategy

Implement comprehensive testing for server actions:

import { createItem } from '@/app/actions'

describe('Server Actions', () => {
  it('should handle valid input correctly', async () => {
    const formData = new FormData()
    formData.append('name', 'Test Item')

    const result = await createItem(formData)
    expect(result.success).toBe(true)
  })

  it('should handle invalid input appropriately', async () => {
    const formData = new FormData()
    const result = await createItem(formData)
    expect(result.success).toBe(false)
    expect(result.error).toBeDefined()
  })

  it('should handle server errors gracefully', async () => {
    // Test error scenarios
  })
})

Conclusion

Next.js Server Actions provide a powerful and secure way to handle form submissions and data mutations. By following the patterns and best practices outlined in this guide, you can build robust applications with excellent user experience and strong security.

Key takeaways:

  • Use Server Actions for form handling and data mutations
  • Implement proper validation and error handling
  • Leverage React hooks for loading and optimistic updates
  • Follow security best practices
  • Ensure progressive enhancement

Additional Resources

  • Next.js Server Actions Documentation
  • React Server Components
  • Next.js Forms Documentation