Handle form submissions with 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.
Form Backend API for Nuxt.js
Section titled “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 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
Section titled “Prerequisites”Before integrating Forminit with your Nuxt.js application:
- Create a Forminit account at forminit.com
- Create a form in your dashboard
- Create an API key
1. Install
Section titled “1. Install”# npm
npm install forminit
# yarn
yarn add forminit
# pnpm
pnpm add forminit
2. Set Authentication Mode to Protected
Section titled “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.
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,
},
});
4. Create API Route
Section titled “4. Create API Route”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.
5. Create Form Component
Section titled “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.
Forminit supports two submission formats: FormData and JSON.
FormData Submission
Section titled “FormData Submission”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 |
JSON Submission
Section titled “JSON Submission”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
Section titled “File Uploads”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 |
Response Structure
Section titled “Response Structure”Success Response
Section titled “Success Response”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 |
Error Response
Section titled “Error Response”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 |
Error Handling
Section titled “Error Handling”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>
Common Error Codes
Section titled “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
Section titled “Composable Pattern”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>
Optional Integrations
Section titled “Optional Integrations”Enhance your forms with additional security:
- reCAPTCHA: forminit.com/docs/recaptcha-integration
- hCaptcha: forminit.com/docs/hcaptcha-integration
- Honeypot: forminit.com/docs/honeypot
Related Documentation
Section titled “Related Documentation”- Form Blocks Reference — Complete reference for all block types
- File Uploads — Detailed file upload guide
- API Reference — Full REST API documentation
- Node.js Integration — Node.js specific setup
- Next.js Integration — Next.js specific setup