Integrations
Published: 17-May-2026

Next.js Form Integration: Server Actions, API Routes, and More

Complete guide to handling form submissions in Next.js with Server Actions, API routes, and client componentsβ€”without building your own backend.

MFC

MyFormConnect Team

15 min read

Next.js is one of the most popular React frameworks for building production-ready web applications. With its powerful features like Server Actions, API Routes, and the App Router, Next.js offers multiple ways to integrate form handling with MyFormConnect.

In this comprehensive guide, we'll explore different integration methods for Next.js, covering everything from simple client-side forms to advanced server-side solutions using Server Actions and API Routes.

πŸ“‹ Prerequisites

Before we begin, make sure you have:

  • A MyFormConnect account (free to get started)
  • A Next.js application (version 13+ with App Router or Pages Router)
  • Your form UUID from the MyFormConnect dashboard
  • Basic knowledge of React and Next.js

πŸ”— Step 1: Get Your Form Endpoint

First, retrieve your form's submission endpoint from your MyFormConnect dashboard:

  1. Navigate to your form settings
  2. Copy your Form UUID
  3. Your endpoint will be: https://myformconnect.io/f/YOUR_FORM_UUID

Store this in your environment variables for security:

# .env.local
NEXT_PUBLIC_MYFORM_UUID=your-form-uuid-here
MYFORM_UUID=your-form-uuid-here  # For server-side use

βš›οΈ Step 2: Client Component Form (App Router)

For client-side form handling in Next.js 13+ App Router, create a client component:

'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [status, setStatus] = useState('idle');
  const [error, setError] = useState(null);

  const FORM_UUID = process.env.NEXT_PUBLIC_MYFORM_UUID;
  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('loading');
    setError(null);

    try {
      const response = await fetch(API_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        setStatus('success');
        setFormData({ name: '', email: '', message: '' });
      } else {
        const errorData = await response.json();
        throw new Error(errorData.error || 'Submission failed');
      }
    } catch (err) {
      setStatus('error');
      setError(err.message);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium mb-1">
          Name
        </label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email
        </label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium mb-1">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          rows="5"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      {status === 'error' && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
          {error || 'Failed to submit form. Please try again.'}
        </div>
      )}

      {status === 'success' && (
        <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
          Thank you! Your message has been sent successfully.
        </div>
      )}

      <button
        type="submit"
        disabled={status === 'loading'}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {status === 'loading' ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

πŸš€ Step 3: Using Server Actions (App Router)

Server Actions are a Next.js 13+ feature that allows you to execute server-side code directly from client components. This is the recommended approach for form submissions:

Create a Server Action

// app/actions/submitForm.ts (or .js)
'use server';

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

  const FORM_UUID = process.env.MYFORM_UUID;
  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  try {
    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name, email, message }),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return { success: false, error: errorData.error || 'Submission failed' };
    }

    return { success: true };
  } catch (error) {
    return { success: false, error: 'Network error. Please try again.' };
  }
}

Use Server Action in Form Component

'use client';

import { useState } from 'react';
import { submitForm } from '@/app/actions/submitForm';

export default function ContactForm() {
  const [status, setStatus] = useState('idle');
  const [error, setError] = useState(null);

  async function handleSubmit(formData: FormData) {
    setStatus('loading');
    setError(null);

    const result = await submitForm(formData);

    if (result.success) {
      setStatus('success');
      // Reset form
      const form = document.getElementById('contact-form') as HTMLFormElement;
      form?.reset();
    } else {
      setStatus('error');
      setError(result.error);
    }
  }

  return (
    <form id="contact-form" action={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium mb-1">
          Name
        </label>
        <input
          type="text"
          id="name"
          name="name"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email
        </label>
        <input
          type="email"
          id="email"
          name="email"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium mb-1">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          rows="5"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      {status === 'error' && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
          {error || 'Failed to submit form. Please try again.'}
        </div>
      )}

      {status === 'success' && (
        <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
          Thank you! Your message has been sent successfully.
        </div>
      )}

      <button
        type="submit"
        disabled={status === 'loading'}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {status === 'loading' ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

πŸ›£οΈ Step 4: Using API Routes (App Router)

API Routes provide another way to handle form submissions. Create an API route in the App Router:

// app/api/submit-form/route.ts (or .js)
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    const FORM_UUID = process.env.MYFORM_UUID;
    const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return NextResponse.json(
        { error: errorData.error || 'Submission failed' },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json(data, { status: 200 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Use API Route in Client Component

'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [status, setStatus] = useState('idle');
  const [error, setError] = useState(null);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('loading');
    setError(null);

    try {
      const response = await fetch('/api/submit-form', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        setStatus('success');
        setFormData({ name: '', email: '', message: '' });
      } else {
        const errorData = await response.json();
        throw new Error(errorData.error || 'Submission failed');
      }
    } catch (err) {
      setStatus('error');
      setError(err.message);
    }
  };

  // ... rest of form JSX (same as previous examples)
}

πŸ“„ Step 5: Integration with Pages Router

If you're using the Pages Router (Next.js 12 or earlier), use API routes:

Pages API Route

// pages/api/submit-form.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const FORM_UUID = process.env.MYFORM_UUID;
  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  try {
    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(req.body),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return res.status(response.status).json(errorData);
    }

    const data = await response.json();
    return res.status(200).json(data);
  } catch (error) {
    return res.status(500).json({ error: 'Internal server error' });
  }
}

βœ… Step 6: Adding Validation with Zod

For robust validation, use Zod with Server Actions:

// First, install Zod: npm install zod

// app/actions/submitForm.ts
'use server';

import { z } from 'zod';

const formSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
});

export async function submitForm(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };

  // Validate with Zod
  const validationResult = formSchema.safeParse(rawData);

  if (!validationResult.success) {
    return {
      success: false,
      error: 'Validation failed',
      errors: validationResult.error.flatten().fieldErrors,
    };
  }

  const FORM_UUID = process.env.MYFORM_UUID;
  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  try {
    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(validationResult.data),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return { success: false, error: errorData.error || 'Submission failed' };
    }

    return { success: true };
  } catch (error) {
    return { success: false, error: 'Network error. Please try again.' };
  }
}

πŸ”§ Step 7: Advanced Features

File Uploads

Handle file uploads with Server Actions:

'use server';

export async function submitFormWithFile(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;
  const file = formData.get('file') as File | null;

  const FORM_UUID = process.env.MYFORM_UUID;
  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  const body = new FormData();
  body.append('name', name);
  body.append('email', email);
  body.append('message', message);
  
  if (file) {
    body.append('file', file);
  }

  try {
    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      body: body, // Don't set Content-Type - browser will set it with boundary
    });

    if (!response.ok) {
      const errorData = await response.json();
      return { success: false, error: errorData.error || 'Submission failed' };
    }

    return { success: true };
  } catch (error) {
    return { success: false, error: 'Network error. Please try again.' };
  }
}

Revalidating Cache

If you're displaying form submissions on your site, you can revalidate the cache after submission:

'use server';

import { revalidatePath } from 'next/cache';

export async function submitForm(formData: FormData) {
  // ... form submission logic ...

  // Revalidate the submissions page after successful submission
  if (response.ok) {
    revalidatePath('/submissions');
    return { success: true };
  }
}

Enhanced Loading States with useFormStatus

Use the useFormStatus hook for better loading states:

'use client';

import { useFormStatus } from 'react-dom';
import { submitForm } from '@/app/actions/submitForm';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
    >
      {pending ? 'Sending...' : 'Send Message'}
    </button>
  );
}

export default function ContactForm() {
  return (
    <form action={submitForm} className="space-y-4">
      {/* Form fields */}
      <SubmitButton />
    </form>
  );
}

πŸ’‘ Best Practices

  • Use Server Actions for form submissions: They're the recommended approach in Next.js 13+ and provide better security and performance.
  • Store sensitive data in server-side environment variables: Use MYFORM_UUID (without NEXT_PUBLIC_) for server-side code.
  • Validate on both client and server: Client-side validation improves UX, but server-side validation is essential for security.
  • Use TypeScript: TypeScript provides better type safety and developer experience.
  • Handle errors gracefully: Always provide user-friendly error messages.
  • Optimize for performance: Use Server Actions to reduce client-side JavaScript and improve performance.
  • Test with different Next.js versions: Make sure your code works with both App Router and Pages Router if needed.

πŸ“Š Server Actions vs API Routes

Feature Server Actions API Routes
Next.js Version 13+ (App Router) All versions
Type Safety βœ… Better with TypeScript ⚠️ Manual typing
Performance βœ… Faster (no API overhead) ⚠️ Additional HTTP request
Complexity βœ… Simpler ⚠️ More boilerplate
Use Case Form submissions, mutations REST APIs, external integrations

πŸ” Troubleshooting

Server Action Errors

If Server Actions aren't working:

  • Ensure you're using Next.js 13+ with the App Router
  • Check that the file has 'use server' directive at the top
  • Verify environment variables are set correctly
  • Check the Next.js console for error messages

API Route Errors

If API routes aren't responding:

  • Verify the route is in the correct directory (app/api/ or pages/api/)
  • Check that the HTTP method matches (POST, GET, etc.)
  • Ensure CORS is handled correctly if needed
  • Check Next.js server logs for errors

Environment Variable Issues

If environment variables aren't working:

  • Restart your Next.js dev server after changing .env.local
  • Use NEXT_PUBLIC_ prefix only for client-side variables
  • Server-side variables shouldn't have the prefix
  • Check that .env.local is in your .gitignore

πŸ“¦ Complete Example

Here's a complete, production-ready example using Server Actions with TypeScript:

// app/actions/submitForm.ts
'use server';

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

const formSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
});

export type FormState = {
  success?: boolean;
  error?: string;
  errors?: Record<string, string[]>;
};

export async function submitForm(
  prevState: FormState | null,
  formData: FormData
): Promise<FormState> {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };

  const validationResult = formSchema.safeParse(rawData);

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

  const FORM_UUID = process.env.MYFORM_UUID;
  if (!FORM_UUID) {
    return { success: false, error: 'Form UUID not configured' };
  }

  const API_ENDPOINT = `https://myformcapture.com/f/${FORM_UUID}`;

  try {
    const response = await fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(validationResult.data),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return { success: false, error: errorData.error || 'Submission failed' };
    }

    revalidatePath('/contact');
    return { success: true };
  } catch (error) {
    return { success: false, error: 'Network error. Please try again.' };
  }
}
// app/components/ContactForm.tsx
'use client';

import { useActionState } from 'react';
import { submitForm, FormState } from '@/app/actions/submitForm';

export default function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitForm, null);

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium mb-1">
          Name *
        </label>
        <input
          type="text"
          id="name"
          name="name"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
          aria-invalid={state?.errors?.name ? 'true' : 'false'}
        />
        {state?.errors?.name && (
          <p className="text-red-600 text-sm mt-1">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email *
        </label>
        <input
          type="email"
          id="email"
          name="email"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
          aria-invalid={state?.errors?.email ? 'true' : 'false'}
        />
        {state?.errors?.email && (
          <p className="text-red-600 text-sm mt-1">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium mb-1">
          Message *
        </label>
        <textarea
          id="message"
          name="message"
          rows="5"
          required
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
          aria-invalid={state?.errors?.message ? 'true' : 'false'}
        />
        {state?.errors?.message && (
          <p className="text-red-600 text-sm mt-1">{state.errors.message[0]}</p>
        )}
      </div>

      {state?.error && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
          {state.error}
        </div>
      )}

      {state?.success && (
        <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
          Thank you! Your message has been sent successfully. We'll get back to you soon.
        </div>
      )}

      <button
        type="submit"
        disabled={isPending}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {isPending ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

βœ… Conclusion

Next.js offers powerful and flexible ways to integrate MyFormConnect, from simple client-side forms to advanced server-side solutions. Whether you choose Server Actions, API Routes, or client components, MyFormConnect provides a reliable backend for handling form submissions.

Key takeaways:

  • βœ… Server Actions are the recommended approach for Next.js 13+ App Router
  • βœ… API Routes work with both App Router and Pages Router
  • βœ… TypeScript and Zod provide excellent type safety and validation
  • βœ… Server-side code keeps sensitive data secure
  • βœ… No backend infrastructure needed β€” just API calls

Ready to get started? Sign up for MyFormConnect and start collecting form submissions in your Next.js app today!

πŸš€ Ready to Integrate MyFormConnect with Next.js?

Create your free MyFormConnect account and start collecting form submissions in your Next.js app in minutes.

Start Free Trial

No credit card required β€’ 5-minute setup β€’ Next.js-ready API