Skip to content

Handle form submissions with Next.js

Forminit+Next.js

Copy AI Instructions

Copy and paste into ChatGPT, Claude, Cursor, or your preferred AI assistant. Includes everything needed to generate forms with Forminit.

Next.js is a React framework that supports both static site generation (SSG) and server-side rendering (SSR). Forminit integrates seamlessly with Next.js through our NPM package, which provides a secure server-side proxy to protect your API keys.

What you’ll get:

  • Accept form submissions without building a backend
  • Email notifications for new submissions
  • Spam protection (reCAPTCHA, hCaptcha, honeypot)
  • File uploads support
  • Submission dashboard to manage responses

Requirements:

  • Next.js 13+
  • Node.js 18+
  • A Forminit account and Form ID

  1. Sign up or log in at forminit.com
  2. Create a new form
  3. Go to Form Settings → Set authentication mode to Protected
  4. Copy your Form ID (e.g., YOUR-FORM-ID)

npm install forminit

  1. Create an API token from Account → API Tokens in your Forminit dashboard
  2. Add the token to .env.local:
FORMINIT_API_KEY=sk_live_xxxxxxxxxxxx

Create a proxy route that securely forwards requests to Forminit:

// app/api/forminit/route.ts

import { createForminitProxy } from 'forminit/next';

const forminit = createForminitProxy({
  apiKey: process.env.FORMINIT_API_KEY,
});

export const POST = forminit.POST;

// components/ContactForm.tsx

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');
    setErrorMessage(null);

    const formData = new FormData(e.currentTarget);
    const { data, error } = await forminit.submit('Xk9mP2nQ1r', formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    setStatus('success');
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <div className="form-group">
        <label htmlFor="firstName">First Name</label>
        <input
          type="text"
          id="firstName"
          name="fi-sender-firstName"
          placeholder="John"
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="lastName">Last Name</label>
        <input
          type="text"
          id="lastName"
          name="fi-sender-lastName"
          placeholder="Doe"
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="fi-sender-email"
          placeholder="john@example.com"
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="fi-text-message"
          placeholder="Your message..."
          rows={5}
          required
        />
      </div>

      {status === 'error' && (
        <p className="status-error">{errorMessage}</p>
      )}
      {status === 'success' && (
        <p className="status-success">Message sent successfully!</p>
      )}

      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

// app/contact/page.tsx

import { ContactForm } from '@/components/ContactForm';

export default function ContactPage() {
  return (
    <div className="contact-page">
      <h1>Contact Us</h1>
      <p>We'd love to hear from you. Fill out the form below and we'll get back to you soon.</p>
      
      <ContactForm />
    </div>
  );
}

Store your Form ID in environment variables:

# .env.local

FORMINIT_API_KEY=sk_live_xxxxxxxxxxxx
NEXT_PUBLIC_FORMINIT_FORM_ID=Xk9mP2nQ1r

Access in your component:

const FORM_ID = process.env.NEXT_PUBLIC_FORMINIT_FORM_ID!;

const { data, error } = await forminit.submit(FORM_ID, formData);

For a complete list of available form blocks (text, email, phone, file, rating, select, etc.) and field naming patterns, see the Form Blocks documentation.


// components/ContactFormWithSubject.tsx

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

export function ContactFormWithSubject() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');

    const formData = new FormData(e.currentTarget);
    const { error } = await forminit.submit('Xk9mP2nQ1r', formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    setStatus('success');
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input type="text" name="fi-sender-firstName" placeholder="First name" required />
      <input type="text" name="fi-sender-lastName" placeholder="Last name" required />
      <input type="email" name="fi-sender-email" placeholder="Email" required />
      
      <select name="fi-select-subject" required>
        <option value="">Select a subject</option>
        <option value="general">General Inquiry</option>
        <option value="support">Support</option>
        <option value="sales">Sales</option>
        <option value="partnership">Partnership</option>
      </select>
      
      <textarea name="fi-text-message" placeholder="Your message" required />
      
      {status === 'error' && <p className="status-error">{errorMessage}</p>}
      {status === 'success' && <p className="status-success">Message sent!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}
// components/NewsletterForm.tsx

'use client';

import { useState } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

export function NewsletterForm() {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setStatus('loading');

    const formData = new FormData();
    formData.append('fi-sender-email', email);

    const { error } = await forminit.submit('aB3cD5eF7g', formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    setStatus('success');
    setEmail('');
  }

  return (
    <form onSubmit={handleSubmit} className="newsletter-form">
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Subscribing...' : 'Subscribe'}
      </button>
      
      {status === 'error' && <p className="status-error">{errorMessage}</p>}
      {status === 'success' && <p className="status-success">Thank you for subscribing!</p>}
    </form>
  );
}
// components/FeedbackForm.tsx

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

export function FeedbackForm() {
  const [rating, setRating] = useState<number | null>(null);
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');

    const formData = new FormData(e.currentTarget);
    const { error } = await forminit.submit('Xk9mP2nQ1r', formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    setStatus('success');
    formRef.current?.reset();
    setRating(null);
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input type="email" name="fi-sender-email" placeholder="Email (optional)" />
      
      <fieldset>
        <legend>How would you rate your experience?</legend>
        {[1, 2, 3, 4, 5].map((n) => (
          <label key={n}>
            <input
              type="radio"
              name="fi-rating-experience"
              value={n}
              checked={rating === n}
              onChange={() => setRating(n)}
            />
            {n}
          </label>
        ))}
      </fieldset>
      
      <textarea name="fi-text-feedback" placeholder="Tell us more (optional)" />
      
      {status === 'error' && <p className="status-error">{errorMessage}</p>}
      {status === 'success' && <p className="status-success">Thank you for your feedback!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Submitting...' : 'Submit Feedback'}
      </button>
    </form>
  );
}
// components/ApplicationForm.tsx

'use client';

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit({ proxyUrl: '/api/forminit' });

export function ApplicationForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');

    const formData = new FormData(e.currentTarget);
    const { error } = await forminit.submit('T4vWx8yZ0q', formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    setStatus('success');
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input type="text" name="fi-sender-firstName" placeholder="First name" required />
      <input type="text" name="fi-sender-lastName" placeholder="Last name" required />
      <input type="email" name="fi-sender-email" placeholder="Email" required />
      <input type="tel" name="fi-sender-phone" placeholder="Phone (+1234567890)" />
      
      <input type="url" name="fi-url-linkedin" placeholder="LinkedIn profile URL" />
      <input type="url" name="fi-url-portfolio" placeholder="Portfolio URL" />
      
      <select name="fi-select-position" required>
        <option value="">Select position</option>
        <option value="frontend">Frontend Developer</option>
        <option value="backend">Backend Developer</option>
        <option value="fullstack">Full Stack Developer</option>
        <option value="designer">UI/UX Designer</option>
      </select>
      
      <label>
        Resume (PDF)
        <input type="file" name="fi-file-resume" accept=".pdf" required />
      </label>
      
      <label>
        Cover Letter (optional)
        <input type="file" name="fi-file-cover_letter" accept=".pdf,.doc,.docx" />
      </label>
      
      <textarea name="fi-text-why_join" placeholder="Why do you want to join us?" />
      
      {status === 'error' && <p className="status-error">{errorMessage}</p>}
      {status === 'success' && <p className="status-success">Application submitted!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Submitting...' : 'Submit Application'}
      </button>
    </form>
  );
}

Instead of FormData, you can submit structured JSON:

const { data, error } = await forminit.submit('Xk9mP2nQ1r', {
  blocks: [
    {
      type: 'sender',
      properties: {
        email: 'john@example.com',
        firstName: 'John',
        lastName: 'Doe',
      },
    },
    {
      type: 'text',
      name: 'message',
      value: 'Hello world',
    },
    {
      type: 'select',
      name: 'subject',
      value: 'general',
    },
  ],
});

The SDK returns { data, redirectUrl, error }:

On success:

{
  data: {
    hashId: "7LMIBoYY74JOCp1k",      // Unique submission ID
    date: "2026-01-01 21:10:24",      // Timestamp
    blocks: {                          // Submitted values
      sender: {
        firstName: "John",
        lastName: "Doe",
        email: "john@example.com"
      },
      message: "Hello from Next.js!"
    }
  },
  redirectUrl: "https://forminit.com/thank-you"
}

On error:

{
  error: {
    error: "ERROR_CODE",
    code: 400,
    message: "Human-readable error message"
  }
}

If you prefer to redirect users to a thank-you page:

import { useRouter } from 'next/navigation';

export function ContactForm() {
  const router = useRouter();
  
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    
    const formData = new FormData(e.currentTarget);
    const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

    if (error) {
      setStatus('error');
      setErrorMessage(error.message);
      return;
    }

    // Redirect to thank-you page
    router.push('/thank-you');
    // Or use Forminit's redirect URL:
    // window.location.href = redirectUrl;
  }
  
  // ...
}

Create a thank-you page:

// app/thank-you/page.tsx

import Link from 'next/link';

export default function ThankYouPage() {
  return (
    <div>
      <h1>Thank You!</h1>
      <p>Thank you for your message! We'll get back to you soon.</p>
      <Link href="/">← Back to Home</Link>
    </div>
  );
}

Here’s a typical Next.js project structure with Forminit forms:

my-nextjs-app/
├── app/
│   ├── api/
│   │   └── forminit/
│   │       └── route.ts           # Forminit proxy route
│   ├── contact/
│   │   └── page.tsx               # Contact page
│   ├── thank-you/
│   │   └── page.tsx               # Thank you page
│   ├── layout.tsx                 # Root layout
│   └── page.tsx                   # Homepage
├── components/
│   ├── ContactForm.tsx            # Contact form component
│   ├── NewsletterForm.tsx         # Newsletter component
│   └── FeedbackForm.tsx           # Feedback component
├── .env.local                     # Environment variables
├── next.config.js                 # Next.js config
├── package.json
└── tsconfig.json

The Forminit SDK includes TypeScript definitions:

import type { ForminitResponse, ForminitError } from 'forminit';

const { data, redirectUrl, error }: ForminitResponse = await forminit.submit(
  formId,
  formData
);

if (error) {
  const typedError: ForminitError = error;
  console.error(typedError.code, typedError.message);
}

  1. View your submissions in the Forminit dashboard
  2. Set up email notifications for new submissions
  3. Explore webhook integrations for advanced workflows