---
title: "Add forms to Nuxt.js"
description: "Add a form backend to Nuxt.js. File uploads, validation, and email notifications included."
url: https://forminit.com/docs/nuxtjs/
---

# Add forms to Nuxt.js

![Forminit+Nuxt.js](https://forminit.com/guides/nuxt-thumbnail.png) 

> **Build with AI:** give your AI coding tool your Form ID and this integration skill guide: https://forminit.com/skills/forminit-nuxtjs/SKILL.md

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

## Form Backend API for Nuxt.js

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.

---

## Prerequisites

Before integrating Forminit with your Nuxt.js application:

1. **Create a Forminit account** at [forminit.com](https://forminit.com)
2. **Create a form** in your dashboard
3. **Create an API key**

---

## 1. Install

```bash
# npm
npm install forminit

# yarn
yarn add forminit

# pnpm
pnpm add forminit
```

## 2. Set Authentication Mode to Protected

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

<img src="/docs/auth-mode-protected.png" alt="Forminit Authentication Mode Protected" style="width: 450px;" />

## 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:

```bash
# .env
FORMINIT_API_KEY="fi_your_secret_api_key"
```

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

> **Caution**
>
> Never commit API keys to version control. Add `.env` to your `.gitignore` file.

## 4. Create API Route

Create a server API route to proxy form submissions securely:

```ts
// 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.

## 5. Create Form Component

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](https://forminit.com/docs/form-blocks/).

Forminit supports two submission formats: **FormData** and **JSON**.

### FormData Submission

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

```vue
<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` |

### JSON Submission

Best for programmatic submissions with structured data:

```vue
<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>
```

> **Caution**
>
> JSON submissions do not support file uploads. Use FormData for forms with file fields.

---

## File Uploads

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

```vue
<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 |

---

## Response Structure

### Success Response

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

```ts
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 |

### Error Response

When submission fails, `error` contains:

```ts
{
  "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 |

---

## Error Handling

Handle errors appropriately in your components:

```vue
<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>
```

### Common Error Codes

| 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 |

---

## Composable Pattern

Create a reusable composable for form submissions:

```ts
// 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:**

```vue
<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>
```

---

## Optional Integrations

Enhance your forms with additional security:

- **Cloudflare Turnstile**: [forminit.com/docs/cloudflare-turnstile/](https://forminit.com/docs/cloudflare-turnstile/)
- **reCAPTCHA**: [forminit.com/docs/recaptcha/](https://forminit.com/docs/recaptcha/)
- **hCaptcha**: [forminit.com/docs/hcaptcha/](https://forminit.com/docs/hcaptcha/)
- **Honeypot**: [forminit.com/docs/honeypot/](https://forminit.com/docs/honeypot/)

---

## Related Documentation

- [Form Blocks Reference](https://forminit.com/docs/form-blocks/) — Complete reference for all block types
- [File Uploads](https://forminit.com/docs/file-upload/) — Detailed file upload guide
- [API Reference](https://forminit.com/docs/submit-form-api/) — Full REST API documentation
- [Node.js Integration](https://forminit.com/docs/nodejs/) — Node.js specific setup
- [Next.js Integration](https://forminit.com/docs/nextjs/) — Next.js specific setup
