Skip to content

Handle form submissions with Nuxt.js

Forminit+Nuxt.js

Copy AI Instructions

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

Learn how to handle form submissions in Nuxt.js using the Forminit SDK.

Forminit provides a complete form backend solution — handling submissions, storage, validation, and notifications so you can focus on building your Nuxt.js application.

Why use Forminit?

  • Zero database setup — All submissions are securely stored and managed for you
  • Instant notifications — Receive email alerts the moment a form is submitted
  • Server-side validation — Email, phone, URL, and country fields are validated automatically
  • Simple file uploads — Accept files up to 25 MB with no storage configuration needed
  • Built-in spam protection — Works with reCAPTCHA, hCaptcha, and honeypot fields
  • Marketing attribution — UTM parameters and referrer data captured automatically
  • Submission dashboard — Browse, search, and export all your form data
  • Webhook integrations — Send submissions to your own endpoints when needed

Skip the boilerplate and ship forms faster. Forminit handles the backend complexity while you build great user experiences.


Before integrating Forminit with your Nuxt.js application:

  1. Create a Forminit account at forminit.com
  2. Create a form in your dashboard
  3. Create an API key

# npm
npm install forminit

# yarn
yarn add forminit

# pnpm
pnpm add forminit

Set authentication to Protected for server-side integrations. This enables higher rate limits and requires the x-api-key header.

Forminit Authentication Mode Protected

3. Create an API Token and Configure Environment

Section titled “3. Create an API Token and Configure Environment”

Generate your secret API token from Account → API Tokens in the Forminit dashboard.

Add the token to your .env file and configure runtime config:

# .env
FORMINIT_API_KEY="fi_your_secret_api_key"
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    forminitApiKey: process.env.FORMINIT_API_KEY,
  },
});

Create a server API route to proxy form submissions securely:

// server/api/forminit.post.ts
import { createForminitNuxtHandler } from 'forminit/nuxt';

const config = useRuntimeConfig();

const forminitHandler = createForminitNuxtHandler({
  apiKey: config.forminitApiKey,
});

export default defineEventHandler(forminitHandler);

This route proxies requests to Forminit, keeping your API key secure on the server.

Forminit uses a block-based system to structure form data. Each submission contains an array of blocks representing different field types.

For complete documentation on all available blocks, field naming conventions, and validation rules, see the Form Blocks Reference.

Forminit supports two submission formats: FormData and JSON.

Best for forms with file uploads or when processing HTML form data:

<script setup lang="ts">
import { ref } from 'vue';
import { Forminit } from 'forminit';

const FORM_ID = 'frm_abc123xyz';

const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const errorMessage = ref<string | null>(null);
const formRef = ref<HTMLFormElement | null>(null);

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

async function handleSubmit() {
  if (!formRef.value) return;
  
  status.value = 'loading';
  errorMessage.value = null;

  const formData = new FormData(formRef.value);
  const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

  if (error) {
    status.value = 'error';
    errorMessage.value = error.message;
    return;
  }

  status.value = 'success';
  formRef.value.reset();
}
</script>

<template>
  <form ref="formRef" @submit.prevent="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 />
    <textarea name="fi-text-message" placeholder="Message" required />
    
    <p v-if="status === 'error'" class="error">{{ errorMessage }}</p>
    <p v-if="status === 'success'" class="success">Message sent!</p>
    
    <button type="submit" :disabled="status === 'loading'">
      {{ status === 'loading' ? 'Sending...' : 'Send' }}
    </button>
  </form>
</template>

FormData Field Naming:

Block TypePatternExample
Sender propertiesfi-sender-{property}fi-sender-email
Field blocksfi-{type}-{name}fi-text-message

Best for programmatic submissions with structured data:

<script setup lang="ts">
import { ref } from 'vue';
import { Forminit } from 'forminit';

const FORM_ID = 'frm_abc123xyz';

const firstName = ref('');
const lastName = ref('');
const email = ref('');
const message = ref('');
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const errorMessage = ref<string | null>(null);

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

async function handleSubmit() {
  status.value = 'loading';
  errorMessage.value = null;

  const { data, redirectUrl, error } = await forminit.submit(FORM_ID, {
    blocks: [
      {
        type: 'sender',
        properties: {
          email: email.value,
          firstName: firstName.value,
          lastName: lastName.value,
        },
      },
      {
        type: 'text',
        name: 'message',
        value: message.value,
      },
    ],
  });

  if (error) {
    status.value = 'error';
    errorMessage.value = error.message;
    return;
  }

  status.value = 'success';
  firstName.value = '';
  lastName.value = '';
  email.value = '';
  message.value = '';
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="firstName" type="text" placeholder="First name" required />
    <input v-model="lastName" type="text" placeholder="Last name" required />
    <input v-model="email" type="email" placeholder="Email" required />
    <textarea v-model="message" placeholder="Message" required />
    
    <p v-if="status === 'error'" class="error">{{ errorMessage }}</p>
    <p v-if="status === 'success'" class="success">Message sent!</p>
    
    <button type="submit" :disabled="status === 'loading'">
      {{ status === 'loading' ? 'Sending...' : 'Send' }}
    </button>
  </form>
</template>

File uploads require FormData. Add file inputs using the fi-file-{name} naming pattern:

<script setup lang="ts">
import { ref } from 'vue';
import { Forminit } from 'forminit';

const FORM_ID = 'frm_abc123xyz';

const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const errorMessage = ref<string | null>(null);
const formRef = ref<HTMLFormElement | null>(null);

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

async function handleSubmit() {
  if (!formRef.value) return;
  
  status.value = 'loading';
  errorMessage.value = null;

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

  if (error) {
    status.value = 'error';
    errorMessage.value = error.message;
    return;
  }

  status.value = 'success';
  formRef.value.reset();
}
</script>

<template>
  <form ref="formRef" @submit.prevent="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 />
    
    <label>Resume (PDF)</label>
    <input type="file" name="fi-file-resume" accept=".pdf,.doc,.docx" required />
    
    <label>Additional Documents (optional)</label>
    <input type="file" name="fi-file-documents[]" multiple />
    
    <p v-if="status === 'error'" class="error">{{ errorMessage }}</p>
    <p v-if="status === 'success'" class="success">Application submitted!</p>
    
    <button type="submit" :disabled="status === 'loading'">
      {{ status === 'loading' ? 'Submitting...' : 'Submit' }}
    </button>
  </form>
</template>

File Field Naming:

PatternExampleDescription
fi-file-{name}fi-file-resumeSingle file upload
fi-file-{name}[]fi-file-documents[]Multiple files (with multiple attribute)

File Upload Limits:

LimitValue
Maximum upload size per submission25 MB
Maximum file blocks per submission20

The SDK returns { data, redirectUrl, error }. On successful submission:

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

// data contains:
{
  "hashId": "7LMIBoYY74JOCp1k",
  "date": "2026-01-01 21:10:24",
  "blocks": {
    "sender": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com"
    },
    "message": "Hello world"
  }
}

// redirectUrl contains the thank you page URL
"https://forminit.com/thank-you"
FieldTypeDescription
data.hashIdstringUnique submission identifier
data.datestringSubmission timestamp (YYYY-MM-DD HH:mm:ss)
data.blocksobjectAll submitted field values
redirectUrlstringThank you page URL

When submission fails, error contains:

{
  "error": "FI_SCHEMA_FORMAT_EMAIL",
  "code": 400,
  "message": "Invalid email format for field: 'contact'. Please enter a valid email address."
}
FieldTypeDescription
errorstringError code identifier
codenumberHTTP status code
messagestringHuman-readable error message

Handle errors appropriately in your components:

<script setup lang="ts">
import { ref } from 'vue';
import { Forminit } from 'forminit';

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

const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const errorMessage = ref<string | null>(null);
const errorField = ref<string | null>(null);

async function handleSubmit(formData: FormData) {
  status.value = 'loading';
  errorMessage.value = null;
  errorField.value = null;

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

    if (error) {
      status.value = 'error';
      
      switch (error.error) {
        case 'FI_SCHEMA_FORMAT_EMAIL':
          errorField.value = 'email';
          errorMessage.value = 'Invalid email address';
          break;
        case 'FI_RULES_PHONE_INVALID':
          errorField.value = 'phone';
          errorMessage.value = 'Invalid phone number format';
          break;
        case 'TOO_MANY_REQUESTS':
          errorMessage.value = 'Please wait before submitting again';
          break;
        default:
          errorMessage.value = error.message;
      }
      return;
    }

    status.value = 'success';
    console.log('Submission ID:', data.hashId);
    
  } catch (err) {
    status.value = 'error';
    errorMessage.value = 'An unexpected error occurred';
    console.error('Submission error:', err);
  }
}
</script>
Error CodeHTTP StatusDescription
FORM_NOT_FOUND404Form ID doesn’t exist or was deleted
FORM_DISABLED403Form is disabled by owner
MISSING_API_KEY401API key required but not provided
EMPTY_SUBMISSION400No fields with values submitted
FI_SCHEMA_FORMAT_EMAIL400Invalid email format
FI_RULES_PHONE_INVALID400Invalid phone number format
FI_SCHEMA_RANGE_RATING400Rating not between 1-5
FI_DATA_COUNTRY_INVALID400Invalid country code
TOO_MANY_REQUESTS429Rate limit exceeded

Create a reusable composable for form submissions:

// composables/useForminit.ts
import { ref } from 'vue';
import { Forminit } from 'forminit';

export function useForminit(formId: string) {
  const forminit = new Forminit({ proxyUrl: '/api/forminit' });
  
  const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
  const error = ref<string | null>(null);
  const submissionId = ref<string | null>(null);

  async function submit(data: FormData | object) {
    status.value = 'loading';
    error.value = null;
    submissionId.value = null;

    const result = await forminit.submit(formId, data);

    if (result.error) {
      status.value = 'error';
      error.value = result.error.message;
      return { success: false, error: result.error };
    }

    status.value = 'success';
    submissionId.value = result.data.hashId;
    return { success: true, data: result.data, redirectUrl: result.redirectUrl };
  }

  function reset() {
    status.value = 'idle';
    error.value = null;
    submissionId.value = null;
  }

  return {
    status,
    error,
    submissionId,
    submit,
    reset,
  };
}

Usage in components:

<script setup lang="ts">
const formRef = ref<HTMLFormElement | null>(null);
const { status, error, submit, reset } = useForminit('frm_abc123xyz');

async function handleSubmit() {
  if (!formRef.value) return;
  
  const formData = new FormData(formRef.value);
  const result = await submit(formData);
  
  if (result.success) {
    formRef.value.reset();
  }
}
</script>

<template>
  <form ref="formRef" @submit.prevent="handleSubmit">
    <!-- form fields -->
    
    <p v-if="status === 'error'">{{ error }}</p>
    <p v-if="status === 'success'">Submitted successfully!</p>
    
    <button type="submit" :disabled="status === 'loading'">
      {{ status === 'loading' ? 'Sending...' : 'Send' }}
    </button>
  </form>
</template>

Enhance your forms with additional security: