DEV Community

Cover image for Next.js Server Actions vs API Routes: Don’t Build Your App Until You Read This
Yogesh Chavan
Yogesh Chavan

Posted on

Next.js Server Actions vs API Routes: Don’t Build Your App Until You Read This

When building applications with Next.js 13+ (App Router), you have two main ways to handle server-side logic:

  • Server Actions: Functions that run on the server, called directly from your components

  • API Routes: Traditional HTTP endpoints that handle requests

Both serve different purposes, and knowing when to use each will help you write better code.

What's the Main Difference?

Server Actions: Think of these as functions you can call directly from your React components. They're great for form submissions and mutations.

API Routes: These are like traditional backend endpoints. External services can call them, and they’re perfect for webhooks or third-party integrations.

Server Actions are newer and more tightly integrated with React. API Routes are more traditional and flexible for external communication.

What Are Server Actions?

Server Actions are asynchronous functions that run on the server. They're marked with the 'use server' directive and can be called directly from Client or Server Components.

// app/actions.js

'use server';

export async function createUser(formData) {
  const name = formData.get('name');
  const email = formData.get('email');

  // Save to database
  await db.users.create({ name, email });

  return { success: true };
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we have created a createUser server action and we can use it in the client component as shown below:

// app/signup/page.js

import { createUser } from '../actions';

export default function SignupPage() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Your name" />
      <input name="email" placeholder="Your email" />
      <button type="submit">Sign Up</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to Use Server Actions

  • Form submissions: Perfect for handling form data

  • Data mutations: Creating, updating, or deleting data

  • Direct component actions: When a component needs to trigger server logic

  • Simple database operations: CRUD operations from your UI

  • Revalidating cache: When you need to refresh cached data after mutations

Server Actions - Pros & Cons

Pros

  • No need to create API endpoints

  • Type-safe with TypeScript

  • Works seamlessly with forms

  • Progressive enhancement (works without JS)

  • Less boilerplate code

Cons

  • Only accessible from your Next.js app

  • Can't be called by external services

  • Not RESTful (no standard HTTP methods)

  • Requires Next.js 13+ App Router

Advanced Server Action Example

// app/actions.js

'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  // Validate data
  if (!title || !content) {
    return { error: 'All fields required' };
  }

  // Save to database
  const post = await db.posts.create({ title, content });

  // Revalidate the posts page to show new post
  revalidatePath('/posts');

  // Redirect to the new post page
  redirect(`/posts/${post.id}`);
}
Enter fullscreen mode Exit fullscreen mode

Server Actions automatically handle CSRF protection and don't expose sensitive logic to the client!

What Are API Routes?

API Routes are server-side endpoints that handle HTTP requests. They're files in the app/api directory that export functions to handle different HTTP methods.

// app/api/users/route.js

import { NextResponse } from 'next/server';

// Handle GET requests
export async function GET(request) {
  const users = await db.users.findMany();
  return NextResponse.json(users);
}

// Handle POST requests
export async function POST(request) {
  const body = await request.json();
  const user = await db.users.create(body);
  return NextResponse.json(user, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we have created HTTP Get and Post method API Routes(Route handlers).

Calling an API Route from the Frontend

// app/users/page.js

'use client'

export default function UsersPage() {
  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await fetch('/api/users', {
      method: 'POST',
      headers: {
          'Content-Type': 'application/json'
      },
      body: JSON.stringify({
          name: 'John',
          email: 'john@example.com'
      })
    });

    const user = await response.json();
    console.log('Created user:', user);
  };

  return <button onClick={handleSubmit}>Create User</button>;
}
Enter fullscreen mode Exit fullscreen mode

When to Use API Routes

  • Webhooks: Receiving data from external services (Stripe, GitHub, etc.)

  • Third-party integrations: When external apps need to access your data

  • Mobile apps: Serving data to iOS/Android applications

  • Public APIs: Creating endpoints others can consume

  • Complex authentication flows: OAuth callbacks, custom auth logic

  • File uploads: Handling multipart form data

  • Streaming responses: Server-sent events or large data transfers

  • Custom HTTP headers: When you need fine control over responses

⚠️ Important: External services like Stripe, GitHub webhooks, or mobile apps CANNOT call Server Actions. They need API Routes!

API Routes — Pros & Cons

Pros

  • Can be called by anyone

  • Standard HTTP methods (GET, POST, PUT, PATCH, DELETE etc.)

  • Works with external services

  • Full control over request/response

  • Can set custom headers

  • Great for webhooks

  • Well-documented pattern

Cons

  • More boilerplate code

  • Need to manually handle validation

  • Requires fetch() calls from frontend

  • Need to manage CORS for external access

  • More files to maintain

  • Separate type definitions needed

Advanced API Route Example

// app/api/posts/[id]/route.js

export async function GET(request, { params }) {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });

  if (!post) {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    );
  }

  return NextResponse.json(post);
}

export async function DELETE(request, { params }) {
  await db.posts.delete({
    where: { id: params.id }
  });

  return NextResponse.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

Quick Decision Guide

Use Server Actions When:

  • Handling form submissions

  • Mutating data from your UI components

  • You want less boilerplate code

  • You need progressive enhancement

  • You're working within your Next.js app only

  • You want automatic type safety

Use API Routes When:

  • Building webhooks for external services

  • Creating a public API

  • Supporting mobile apps

  • You need RESTful endpoints

  • Handling file uploads

  • You need custom HTTP headers/status codes

  • OAuth or complex authentication flows

Can You Use Both? Absolutely! Many apps use Server Actions for form handling and UI mutations, while using API Routes for external integrations and webhooks.

Real-World Example: E-commerce App

  • Server Action: Adding items to cart from product page

  • Server Action: Submitting checkout form

  • API Route: Stripe webhook for payment confirmation

  • API Route: Mobile app endpoint to fetch products

🔒 Security Tip: Server Actions have built-in CSRF protection. For API Routes accessed externally, implement your own authentication/protection.

Common Mistakes To Avoid

Mistake 1: Using API Routes for Internal Forms

// DON'T do this - unnecessarily complex!

const handleSubmit = async () => {
  await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(data)
  });
};
Enter fullscreen mode Exit fullscreen mode
// DO this instead - simpler and better!

import { createUser } from './actions';

<form action={createUser}>
  <input name="name" />
  <button>Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Trying to Call Server Actions from External Services

Server Actions cannot be called via HTTP requests from outside your app. If you need external access, use API Routes.

Mistake 3: Not Using revalidatePath() After Mutations

// DON'T forget to revalidate!

export async function updatePost(id, data) {
  await db.posts.update({
    where: { id },
    data
  });

  // Page will show stale/old data!
}
Enter fullscreen mode Exit fullscreen mode
// DO revalidate to show fresh data!

import { revalidatePath } from 'next/cache';

export async function updatePost(id, data) {
  await db.posts.update({
    where: { id },
    data
  });

  revalidatePath('/posts');
}
Enter fullscreen mode Exit fullscreen mode

Advanced Examples

Example 1: Server Action with Client-Side State

// app/components/AddTodoForm.js

'use client'

import { useFormState, useFormStatus } from 'react-dom';
import { addTodo } from '../actions';

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

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Adding...' : 'Add Todo'}
    </button>
  );
}

export default function AddTodoForm() {
  const [state, formAction] = useFormState(addTodo, null);

  return (
    <form action={formAction}>
      <input name="title" required />

      {state?.error && <p>{state.error}</p>}

      <SubmitButton />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example 2: API Route with Authentication

// app/api/admin/users/route.js

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';

export async function GET(request) {
  // Check if user is authenticated
  const session = await getServerSession();

  if (!session || session.user.role !== 'admin') {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const users = await db.users.findMany();
  return NextResponse.json(users);
}
Enter fullscreen mode Exit fullscreen mode

Learn To Build Headshot Generator App Using Next.js + AI

Resources for Learning More

Final Tip: When in doubt, ask yourself: "Will anything outside my Next.js app need to call this?" If yes → API Route. If no → Server Action.

If you found this article useful, feel free to give claps👏 and share your views in the comment.

Connect With Me

Top comments (0)