Next.js Server Actions vs API Routes: When to Use Each
technology

Next.js Server Actions vs API Routes: When to Use Each

Understand the differences between Server Actions and API Routes in Next.js 15. Learn when to use each approach with real-world examples and performance comparisons.

2026-03-10
10 min read
Next.js Server Actions vs API Routes: When to Use Each

Next.js Server Actions vs API Routes: When to Use Each#

Next.js 15 gives you two ways to handle server-side logic: Server Actions and API Routes. Both work, but they're designed for different use cases. Choosing the wrong one leads to unnecessary complexity or missing features.

This guide teaches you when to use each approach.

Understanding the Differences#

Server Actions#

Server Actions are async functions that run on the server and are called directly from components.

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  const result = await supabase
    .from('posts')
    .insert({ title, content, user_id: userId })

  if (result.error) throw result.error
  return result.data
}

API Routes#

API Routes are HTTP endpoints that handle requests and responses.

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const body = await request.json()
  const { title, content } = body

  const result = await supabase
    .from('posts')
    .insert({ title, content, user_id: userId })

  if (result.error) {
    return NextResponse.json({ error: result.error.message }, { status: 400 })
  }

  return NextResponse.json(result.data)
}

Key Differences#

Server Actions:

  • Direct function calls from components
  • Built-in CSRF protection
  • Better type safety
  • Simpler for form submissions
  • No middleware support
  • Can only be called from your app

API Routes:

  • HTTP endpoints
  • Manual CSRF protection
  • Manual type definitions
  • Better for external integrations
  • Full middleware support
  • Can be called from anywhere

When to Use Server Actions#

Use Server Actions for:

1. Form Submissions#

Server Actions are perfect for handling form data directly from components without HTTP overhead.

'use server'

export async function updateProfile(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string

  const result = await supabase
    .from('profiles')
    .update({ name, email })
    .eq('id', userId)

  if (result.error) throw result.error
  return result.data
}

2. Simple Mutations#

For straightforward database operations, Server Actions are cleaner and faster.

'use server'

export async function deletePost(postId: string) {
  const result = await supabase
    .from('posts')
    .delete()
    .eq('id', postId)

  if (result.error) throw result.error
  return result.data
}

3. Keeping Secrets Secure#

API keys stay on the server and are never exposed to the client.

'use server'

export async function sendEmail(email: string) {
  const response = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`
    }
  })

  return response.json()
}

When to Use API Routes#

Use API Routes for:

1. Webhooks#

External services need HTTP endpoints to call your app.

export async function POST(request: NextRequest) {
  const event = await request.json()

  if (event.type === 'charge.succeeded') {
    await supabase
      .from('payments')
      .insert({ stripe_id: event.data.id, status: 'completed' })
  }

  return NextResponse.json({ received: true })
}

2. External Integrations#

Third-party services call your API with authentication.

export async function POST(request: NextRequest) {
  const apiKey = request.headers.get('authorization')

  if (apiKey !== process.env.EXTERNAL_API_KEY) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const data = await request.json()
  return NextResponse.json({ success: true })
}

3. Complex Business Logic#

API Routes support middleware and fine-grained control.

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { userId, amount } = body

    if (amount < 0) {
      return NextResponse.json({ error: 'Invalid amount' }, { status: 400 })
    }

    const result = await supabase
      .from('transactions')
      .insert({ user_id: userId, amount })

    return NextResponse.json(result.data)
  } catch (error) {
    return NextResponse.json({ error: 'Server error' }, { status: 500 })
  }
}

4. Rate Limiting#

Middleware can enforce rate limits on API endpoints.

import { Ratelimit } from '@upstash/ratelimit'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 h')
})

export async function POST(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'
  const result = await ratelimit.limit(ip)

  if (!result.success) {
    return NextResponse.json({ error: 'Rate limited' }, { status: 429 })
  }

  return NextResponse.json({ success: true })
}

Decision Tree#

Ask yourself these questions:

  1. Is it a form submission? → Use Server Actions
  2. Does an external service need to call your app? → Use API Routes
  3. Do you need middleware or rate limiting? → Use API Routes
  4. Do you need fine-grained HTTP control? → Use API Routes
  5. Otherwise → Use Server Actions

Error Handling#

Server Actions#

'use client'

async function handleSubmit(formData: FormData) {
  try {
    const result = await createPost(formData)
    console.log('Success:', result)
  } catch (error) {
    console.error('Error:', error.message)
  }
}

API Routes#

'use client'

async function handleSubmit(formData: FormData) {
  try {
    const response = await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(Object.fromEntries(formData))
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.message)
    }

    const result = await response.json()
    console.log('Success:', result)
  } catch (error) {
    console.error('Error:', error.message)
  }
}

Best Practices#

Server Actions:

  • Use for form submissions
  • Keep them simple and focused
  • Use revalidatePath for cache invalidation
  • Throw errors for error handling
  • Use FormData for file uploads

API Routes:

  • Use for webhooks and external integrations
  • Implement proper error handling
  • Add rate limiting for public endpoints
  • Validate all input
  • Return appropriate HTTP status codes

Conclusion#

Server Actions and API Routes both have their place. Use Server Actions for simple mutations and form submissions—they're simpler and have better type safety. Use API Routes for webhooks, external integrations, and complex business logic.

The key is understanding the trade-offs. Server Actions are simpler but less flexible. API Routes are more complex but more powerful. Choose the right tool for the job.

Frequently Asked Questions

|

Have more questions? Contact us