DEV Community

Cover image for Ditch API Routes: Server Actions in Next.js ⚡
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Ditch API Routes: Server Actions in Next.js ⚡

The API Boilerplate Tax

For years, building a simple contact form at Smart Tech Devs required a massive amount of architectural boilerplate. You had to create a React component, write a onSubmit handler, manually construct a fetch() request, manage isLoading and isError states, and build a dedicated /api/contact route on your backend.

Worse, keeping the data types synchronized between the frontend form and the backend API required manually sharing TypeScript interfaces. If the backend changed a field from string to number, the frontend wouldn't know until the network request failed in production. To build lightning-fast, perfectly synchronized frontends, we must eliminate the API middleman using Next.js Server Actions and Zod.

The Solution: RPC via Server Actions

Next.js Server Actions allow you to call a server-side asynchronous PHP/Node function directly from a client-side React component. The framework automatically handles the underlying network request, the serialization, and the CSRF protection. You completely bypass the need to build a manual API route.

By pairing this with Zod (a TypeScript schema validation library), we can achieve absolute, end-to-end type safety in a single file.

Architecting the Server Action

First, we define our Zod schema and our Server Action. Notice the "use server" directive. This tells Next.js that this function must never be shipped to the browser; it executes exclusively in a secure backend environment.


// actions/userActions.ts
"use server";

import { z } from 'zod';
import db from '@/lib/db';
import { revalidatePath } from 'next/cache';

// 1. ✅ THE ENTERPRISE PATTERN: The Single Source of Truth
// We define the validation rules once. Both the client and server will use this.
export const userSchema = z.object({
    name: z.string().min(2, "Name must be at least 2 characters"),
    email: z.string().email("Invalid email address"),
});

// 2. The Server Action
export async function createUser(formData: FormData) {
    // 3. Extract and strongly type the incoming form data
    const rawData = {
        name: formData.get('name'),
        email: formData.get('email'),
    };

    // 4. Validate securely on the server
    const validatedData = userSchema.safeParse(rawData);

    if (!validatedData.success) {
        return { 
            error: "Validation failed", 
            details: validatedData.error.flatten().fieldErrors 
        };
    }

    // 5. Execute secure backend logic (DB writes, external APIs)
    await db.user.create({ data: validatedData.data });

    // 6. Tell Next.js to purge the cache and update the UI instantly
    revalidatePath('/users');
    
    return { success: true };
}

Consuming the Action in React

Now, we simply pass our Server Action directly to the native HTML action attribute of our form. No fetch(), no manual JSON serialization.


// components/UserForm.tsx
"use client";

import { useRef } from 'react';
import { createUser } from '@/actions/userActions';
import { useFormStatus } from 'react-dom';

function SubmitButton() {
    // Natively tracks the pending state of the Server Action!
    const { pending } = useFormStatus();
    
    return (
        <button 
            type="submit" 
            disabled={pending}
            className="bg-purple-600 text-white px-4 py-2 rounded"
        >
            {pending ? 'Saving...' : 'Create User'}
        </button>
    );
}

export default function UserForm() {
    const ref = useRef<HTMLFormElement>(null);

    // We pass the Server Action directly. Next.js handles the POST request automatically.
    return (
        <form 
            ref={ref}
            action={async (formData) => {
                const result = await createUser(formData);
                if (result.success) ref.current?.reset();
                if (result.error) alert('Error saving user');
            }} 
            className="flex flex-col gap-4 max-w-md p-6 bg-white shadow rounded-xl"
        >
            <input type="text" name="name" placeholder="Full Name" className="border p-2" required />
            <input type="email" name="email" placeholder="Email Address" className="border p-2" required />
            
            <SubmitButton />
        </form>
    );
}

The Engineering ROI

By migrating from manual REST API endpoints to Next.js Server Actions, you strip thousands of lines of boilerplate from your codebase. Paired with Zod, you achieve mathematical end-to-end type safety, ensuring that your frontend form and your backend database queries can never drift out of sync.

Top comments (0)