Skip to content

Add forms to Nuxt.js

Forminit+Nuxt.js

Build with AI

Paste this prompt into your AI coding tool or app builder. It points the agent to the Forminit setup guide, which wires your form and keeps your design. Your form's page in the dashboard has the same prompt with the Form ID filled in.

SETUP PROMPT

Connect my form to Forminit (forminit.com, a form backend). If I have no form yet, ask me which fields I want, then create it.

Form ID: ask me for it before writing code (it is on my form's page at app.forminit.com).

Get the Forminit setup guide, then follow it:
1. If you can run terminal commands, install it: npx skills add forminit/skills --skill forminit-setup -a universal claude-code -y. Then read .agents/skills/forminit-setup/SKILL.md.
2. Otherwise read https://forminit.com/setup.md in full.
3. If you can do neither, stop and tell me.

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 Cloudflare Turnstile, 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 Type Pattern Example
Sender properties fi-sender-{property} fi-sender-email
Field blocks fi-{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:

Pattern Example Description
fi-file-{name} fi-file-resume Single file upload
fi-file-{name}[] fi-file-documents[] Multiple files (with multiple attribute)

File Upload Limits:

Limit Value
Maximum upload size per submission 25 MB
Maximum file blocks per submission 20

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"
Field Type Description
data.hashId string Unique submission identifier
data.date string Submission timestamp (YYYY-MM-DD HH:mm:ss)
data.blocks object All submitted field values
redirectUrl string Thank 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."
}
Field Type Description
error string Error code identifier
code number HTTP status code
message string Human-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 Code HTTP Status Description
FORM_NOT_FOUND 404 Form ID doesn’t exist or was deleted
FORM_DISABLED 403 Form is disabled by owner
MISSING_API_KEY 401 API key required but not provided
EMPTY_SUBMISSION 400 No fields with values submitted
FI_SCHEMA_FORMAT_EMAIL 400 Invalid email format
FI_RULES_PHONE_INVALID 400 Invalid phone number format
FI_SCHEMA_RANGE_RATING 400 Rating not between 1-5
FI_DATA_COUNTRY_INVALID 400 Invalid country code
TOO_MANY_REQUESTS 429 Rate 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:


View this page as Markdown