bezierly-forms
v0.0.5
Published
The official Bezierly SDK — headless forms infrastructure for developers
Downloads
1,031
Maintainers
Readme
bezierly-forms
The official SDK for Bezierly — headless forms infrastructure for modern developers.
Bezierly handles everything after someone clicks Submit. You write the UI using your favourite framework (React, Vue, Svelte, Angular, or vanilla JS/HTML). We handle:
- Submission storage, filtering, and query APIs
- Client-side & server-side schema validation
- Secure direct-to-cloud file uploads with progress tracking
- Multi-provider CAPTCHA / bot protection (Cloudflare Turnstile, hCaptcha, Google reCAPTCHA v3)
- Webhooks, integrations & email notifications
- Real-time analytics, event tracking & conversion metrics
- Rate limiting, spam protection & quota enforcement
Installation
npm install bezierly-forms
# or
yarn add bezierly-forms
# or
pnpm add bezierly-formsFramework Integration
bezierly-forms ships dedicated subpath exports for popular frontend frameworks with automatic state management, validation feedback, and built-in CAPTCHA handling.
React / Next.js (bezierly-forms/react)
'use client';
import { useBezierlyForm, BezierlyFieldError, BezierlyStatus } from 'bezierly-forms/react';
export function ContactForm() {
const { submit, submitting } = useBezierlyForm('frm_contact_123');
return (
<form onSubmit={submit}>
<label>
Email
<input name="email" type="email" placeholder="[email protected]" required />
</label>
<BezierlyFieldError name="email" />
<label>
Message
<textarea name="message" placeholder="How can we help?" required />
</label>
<BezierlyFieldError name="message" />
<button type="submit" disabled={submitting}>
{submitting ? 'Sending…' : 'Send Message'}
</button>
<BezierlyStatus />
</form>
);
}Vue 3 (bezierly-forms/vue)
<script setup>
import { useBezierlyForm, BezierlyFieldError, BezierlyStatus } from 'bezierly-forms/vue';
const { submit, submitting } = useBezierlyForm('frm_contact_123');
</script>
<template>
<form @submit="submit">
<input name="name" placeholder="Your Name" required />
<BezierlyFieldError name="name" />
<input name="email" type="email" placeholder="[email protected]" required />
<BezierlyFieldError name="email" />
<button type="submit" :disabled="submitting">
{{ submitting ? 'Sending…' : 'Submit' }}
</button>
<BezierlyStatus />
</form>
</template>Svelte (bezierly-forms/svelte)
<script>
import { useBezierlyForm } from 'bezierly-forms/svelte';
const { submit, submitting, fieldErrors, status } = useBezierlyForm('frm_contact_123');
</script>
<form on:submit={submit}>
<input name="email" type="email" placeholder="[email protected]" required />
{#if $fieldErrors.email}
<p class="error">{$fieldErrors.email}</p>
{/if}
<button type="submit" disabled={$submitting}>
{$submitting ? 'Sending…' : 'Submit'}
</button>
{#if $status}
<p class="status">{$status.message}</p>
{/if}
</form>Angular (bezierly-forms/angular)
import { Component } from '@angular/core';
import { injectBezierlyForm } from 'bezierly-forms/angular';
@Component({
selector: 'app-contact-form',
standalone: true,
template: `
<form (submit)="form.submit($event)">
<input name="email" type="email" placeholder="[email protected]" required />
@if (form.fieldErrors()['email']) {
<p class="error">{{ form.fieldErrors()['email'] }}</p>
}
<button type="submit" [disabled]="form.submitting()">
{{ form.submitting() ? 'Sending…' : 'Submit' }}
</button>
@if (form.status()) {
<p class="status">{{ form.status()!.message }}</p>
}
</form>
`,
})
export class ContactFormComponent {
readonly form = injectBezierlyForm('frm_contact_123');
}Core Client API (bezierly-forms)
1. Form-Scoped Client (client.form()) — recommended
The canonical pattern: scope a client to one form once, then call .submit(), .validate(), .upload(), etc. on it as many times as you need. Every method — including CAPTCHA auto-acquisition — behaves identically to the shorthand below, since the shorthand is implemented in terms of this.
import { createClient } from 'bezierly-forms';
const bezierly = createClient();
const form = bezierly.form('frm_contact_123');
// Pre-check validation
const validation = await form.validate({ email: 'invalid' });
if (!validation.valid) {
console.log(validation.errors);
}
// Submit
const result = await form.submit({ name: 'Alex', email: '[email protected]' });
console.log(result.submissionId); // "sub_..."2. client.forms.submit() — equivalent shorthand
For a one-off submission where you don't need the scoped client, bezierly.forms.submit(formId, data, options) is exactly bezierly.form(formId).submit(data, options) — same pipeline, same automatic CAPTCHA handling, just without binding form to a variable first:
import { createClient } from 'bezierly-forms';
const bezierly = createClient();
const result = await bezierly.forms.submit('frm_contact_123', {
name: 'Jane Doe',
email: '[email protected]',
message: 'Hello!',
});
console.log(result.submissionId); // "sub_..."3. File Uploads with Progress
const jobsForm = bezierly.form('frm_jobs_123');
const fileRef = await jobsForm.upload(
'resume',
resumeFile,
{
onProgress: (pct) => console.log(`Upload progress: ${pct}%`),
signal: abortController.signal,
}
);
await jobsForm.submit({
name: 'Jane Doe',
resume: fileRef,
});4. Advanced Submit Options & Lifecycle Hooks
const result = await form.submit(formData, {
// Idempotency for safe retries
idempotencyKey: crypto.randomUUID(),
// Custom cancellation signal & timeout
signal: abortController.signal,
timeout: 15000,
// Unified progress (validating -> uploading -> submitting -> complete)
onProgress: (progress) => {
console.log(`Phase: ${progress.phase}, Progress: ${progress.percent}%`);
},
// Transform / enrich payload before sending
beforeSubmit: async (data) => {
return { ...data, utm_source: 'newsletter' };
},
// After submit callback
afterSubmit: async ({ result, data }) => {
console.log('Submitted successfully', result.submissionId);
},
});CAPTCHA
Enable CAPTCHA (Turnstile, hCaptcha, or reCAPTCHA v3) for a form in the Bezierly dashboard — no code changes are required for invisible mode, and only one extra line for checkbox mode. The SDK never needs the provider or site key hardcoded; it reads them from your form's own configuration.
Invisible mode — fully automatic everywhere, including the raw client:
// Framework adapters (useBezierlyForm etc.) and bezierly.form(id).submit() alike —
// nothing to do. A token is silently acquired and attached before every submit.
const { submit } = useBezierlyForm('frm_contact_123');Checkbox mode — also fully automatic in every framework adapter. The widget is mounted directly into the submitted <form> element (right above the submit button, or into your own <div class="bezierly-captcha"> if you add one) the first time the form is submitted. That first attempt is held — status reports "Please complete the CAPTCHA verification, then submit again." — until the widget is solved, then the next submit goes through normally:
'use client';
import { useBezierlyForm, BezierlyStatus } from 'bezierly-forms/react';
export function ContactForm() {
// No <BezierlyCaptcha /> component, no ref, no extra markup — if the form's
// CAPTCHA is checkbox mode, the widget just appears.
const { submit, submitting } = useBezierlyForm('frm_contact_123');
return (
<form onSubmit={submit}>
<input name="email" type="email" required />
<button type="submit" disabled={submitting}>Submit</button>
<BezierlyStatus />
</form>
);
}Raw client (client.form(id).submit()) — invisible mode only. submit() gets no <form>/event reference (just a formId and a data object), and unlike the framework adapters and submit.js, it doesn't render or manage a checkbox widget for you at all — there's no automatic path here for checkbox mode. For a checkbox-mode form, render the provider's widget yourself (get provider/siteKey from form.meta()) and pass the resulting value as captchaToken:
const form = bezierly.form('frm_contact_123');
// Invisible mode: nothing to do, same as everywhere else.
await form.submit({ email });
// Checkbox mode: acquire the token yourself, then pass it explicitly.
await form.submit({ email }, { captchaToken: tokenFromYourOwnWidget });Authenticating with a secret API key instead? CAPTCHA is skipped entirely. Prefer not to manage a checkbox widget by hand at all? Use a framework adapter or submit.js, both of which do that automatically.
A solved checkbox token is single-use: it's automatically reset after a successful submission or any failure except a validation error (an invalid field doesn't waste an already-solved CAPTCHA — the same token can be reused once the field is fixed).
Server & Admin API (bezierly-forms/admin)
For secure, server-side data access (Next.js server actions, Node.js backend, CI scripts), use the dedicated bezierly-forms/admin entry point. It requires an API secret key (bzforms_live_...).
import { createAdminClient } from 'bezierly-forms/admin';
const admin = createAdminClient({
apiKey: process.env.BEZIERLY_FORMS_SECRET_KEY!,
});
// List submissions
const page = await admin.submissions.list('frm_contact_123', {
limit: 20,
status: 'unread',
});
console.log(page.submissions);
// Get single submission
const submission = await admin.submissions.get('frm_contact_123', 'sub_abc123');
// Export submissions
const csvData = await admin.submissions.export('frm_contact_123', 'csv');Structured Errors
All SDK errors inherit from BezierlyError and provide rich debugging context:
import {
FormValidationError,
QuotaExceededError,
FormClosedError,
RateLimitError,
NetworkError
} from 'bezierly-forms';
try {
await bezierly.form('frm_contact_123').submit(data);
} catch (error) {
if (error instanceof FormValidationError) {
// Client or server schema validation failure
console.error('Validation errors:', error.errors);
} else if (error instanceof QuotaExceededError) {
console.error('Submission quota reached for current billing cycle');
} else if (error instanceof FormClosedError) {
console.error('This form is currently closed');
} else if (error instanceof RateLimitError) {
console.warn(`Rate limited. Retry after ${error.retryAfter}s`);
}
}License
MIT © Bezierly
