# Implement Custom Form Handlers with Instatic Semantic Validation: A Complete Guide

> Learn to implement custom form handlers with Instatic semantic validation. Reuse Instatic's TypeBox-driven engine in your own endpoints for robust form processing.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-02

---

**Instatic's TypeBox-driven validation engine can be imported directly from [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts) and reused in custom endpoints, allowing you to bypass the default `/_instatic/form/submit` flow while maintaining the same semantic guarantees.**

Instatic provides a fully-featured CMS-native form system that authors use to build semantic HTML forms from primitive nodes. While the default flow relies on built-in endpoints at `/_instatic/form/challenge` and `/_instatic/form/submit`, you can implement custom form handlers with Instatic semantic validation to integrate external services or apply custom business logic.

## How Instatic's Native Form System Works

The form architecture consists of several tightly coupled components that handle everything from schema definition to runtime submission.

**Form primitives** such as `base.form`, `base.input`, and `base.select` reside in `src/modules/base/forms/` and are registered via [`src/modules/base/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/index.ts). At publish time, the editor generates a **form snapshot** ([`src/core/forms/snapshot.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/snapshot.ts)) that captures the structure and bindings.

During runtime, the browser loads a JavaScript module from [`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts) that injects hidden `data-instatic-page-token` and `data-instatic-page-id` fields into each form. By default, submissions route through two endpoints defined in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts): `POST /_instatic/form/challenge` issues an HMAC-signed token, and `POST /_instatic/form/submit` validates inputs and creates `data_rows` records.

The core validation logic lives in [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts), which performs type coercion, constraint checking, and global limit enforcement.

## When to Use a Custom Handler

Switching to a custom handler bypasses the default endpoints while preserving validation integrity. Consider implementing custom form handlers with Instatic semantic validation when you need:

- **Custom action URLs** – Setting the form's mode to `custom` renders a standard `<form action="…" method="…">` tag, directing the browser to post directly to your specified endpoint instead of `/_instatic/form/submit`.
- **Additional server-side logic** – Integrate with external APIs, write audit logs, or apply business rules that extend beyond the default validation scope.
- **Reusable validation** – Maintain semantic guarantees by importing Instatic's validation routine into your custom endpoint rather than reimplementing constraint logic from scratch.

## Hooking Into Instatic's Validation Engine

### The validateFormSubmission Function

The public validator is exported from [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts) and accepts the same schema definitions used by the native form editor.

```typescript
// src/core/forms/validation.ts
export function validateFormSubmission(input: {
  table: DataTable;
  controls: FormControlBinding[];
  values: Record<string, unknown>;
  limits?: FormSubmissionLimits;
}): FormValidationResult { … }

```

This function coordinates type coercion via `coerceFieldValue` and constraint validation through `validateCoercedValue`, returning a structured result that indicates success or detailed field-level errors.

### Complete Custom Handler Implementation

Your custom endpoint can import the validator and execute the same checks as the built-in handler before persisting data elsewhere.

```typescript
import { validateFormSubmission } from '@core/forms/validation';
import { getDataTableById } from '@core/data/repositories';

// Example custom handler (e.g. in server/api/customForm.ts)
export async function handleCustomForm(req: Request) {
  const payload = await req.json(); // { tableId, values }

  // 1️⃣ Load the target DataTable definition (schema)
  const table = await getDataTableById(payload.tableId);
  if (!table) {
    return new Response(JSON.stringify({ error: 'Table not found' }), { status: 404 });
  }

  // 2️⃣ Build the list of form controls (same shape the editor uses)
  const controls = table.fields.map((field) => ({
    fieldId: field.id,
    name: field.name,
    required: field.required,
    // …other UI-specific props can be added here
  }));

  // 3️⃣ Run Instatic's validation
  const result = validateFormSubmission({
    table,
    controls,
    values: payload.values,
  });

  // 4️⃣ Return validation errors or persist the row
  if (!result.ok) {
    return new Response(JSON.stringify({ errors: result.errors }), { status: 400 });
  }

  // Persist the row (example using the data-row repo)
  const rowId = await createDataRow(table.id, result.cells);
  return new Response(JSON.stringify({ rowId }), { status: 201 });
}

```

### What the Validator Checks

The `validateFormSubmission` function enforces multiple validation layers defined in [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts):

- **Unknown field rejection** – Extra fields not defined in the schema are caught early ([validation.ts L45-L54]).
- **Required field enforcement** – Missing values for required controls trigger errors ([validation.ts L81-L97]).
- **Type coercion** – Values are coerced to their declared types (number, boolean, multi-select, media, relation) via `coerceFieldValue` ([validation.ts L10-L48]).
- **Constraint validation** – String length, min/max limits, regex patterns, email/URL formats, and numeric ranges are verified by `validateCoercedValue` ([validation.ts L71-L119]).
- **Option whitelisting** – Select and multi-select values are validated against allowed option lists ([validation.ts L121-L133]).
- **Global limits** – Submission size and field count caps prevent abuse ([validation.ts L27-L33]).

## Configuring Custom Mode in the Editor

Authors toggle between CMS-native and custom submission flows through the **Form Settings Panel** located at [`src/admin/pages/site/panels/PropertiesPanel/FormSettingsPanel.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/panels/PropertiesPanel/FormSettingsPanel.tsx).

When `mode` is set to `custom`, the runtime module in [`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts) emits a conventional HTML form tag using the `action` and `method` attributes you specify, removing the dependency on Instatic's challenge/submit endpoints.

```typescript
// src/admin/pages/site/panels/PropertiesPanel/FormSettingsPanel.tsx
<FormModeSelector
  value={formNode.props.mode}
  onChange={(mode) => updateNodeProps(nodeId, { mode })}
/>

```

## Implementation Workflow

Follow this sequence to deploy a production-ready custom handler:

1. **Author the form** in the visual editor, populate fields, and switch the **Mode** setting to *Custom* while specifying your endpoint URL.
2. **Publish the site** – The form snapshot retains the full control definitions and DataTable bindings, which your endpoint can reference via the CMS API or embedded JSON.
3. **Handle submission** – The browser posts form data directly to your custom URL. Parse the payload and invoke `validateFormSubmission` with the retrieved schema and posted values.
4. **Process results** – Return validation errors in the standard format on failure, or persist the validated `result.cells` to `data_rows` or external systems on success.

## Key Source Files

Understanding these modules is essential for maintaining parity with Instatic's native behavior:

- **[`src/modules/base/forms/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/index.ts)** – Registers all form primitive modules including inputs and selects.
- **[`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts)** – Exports `validateFormSubmission` and `coerceFieldValue` for reuse in custom handlers.
- **[`src/core/forms/targets.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/targets.ts)** – Defines which data tables are valid targets for CMS form submissions.
- **[`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts)** – Reference implementation of the default challenge and submit endpoints.
- **[`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts)** – Injects security tokens and determines whether to render native or custom form markup.
- **[`src/admin/pages/site/panels/PropertiesPanel/FormSettingsPanel.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/panels/PropertiesPanel/FormSettingsPanel.tsx)** – UI component for switching between CMS and Custom modes.

## Summary

- Import `validateFormSubmission` from [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts) to enforce Instatic's semantic validation in any custom endpoint.
- Set the form `mode` to `custom` via [`FormSettingsPanel.tsx`](https://github.com/CoreBunch/Instatic/blob/main/FormSettingsPanel.tsx) to generate standard HTML form posts instead of using `/_instatic/form/submit`.
- Construct the `controls` array from your `DataTable` schema to ensure the validator recognizes all fields and constraints.
- Handle type coercion, required fields, pattern matching, and global limits automatically by delegating to Instatic's validation engine.
- Return standardized error objects to the frontend to maintain compatibility with Instatic's error display components.

## Frequently Asked Questions

### Can I use Instatic validation without the default submit endpoint?

Yes. The `validateFormSubmission` function exported from [`src/core/forms/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/forms/validation.ts) is designed to be environment-agnostic. Import it into any server-side handler, pass the `DataTable` schema and submitted values, and receive the same validation results the native endpoint uses.

### What happens to security tokens when using custom mode?

When `mode` is set to `custom`, the runtime script in [`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts) still injects the `data-instatic-page-token` and `data-instatic-page-id` hidden fields. Your custom endpoint can optionally verify these tokens against the challenge endpoint, or you may implement your own CSRF protection scheme.

### How do I access the form schema in my custom handler?

Retrieve the `DataTable` definition using `getDataTableById` from `@core/data/repositories` as shown in the implementation example. The schema includes field definitions, constraints, and control bindings necessary to construct the validation input.

### Does custom mode support rate limiting?

The built-in rate limiting applied at `POST /_instatic/form/challenge` only governs the default submission flow. Custom endpoints must implement their own rate-limiting middleware or logic, as the browser posts directly to your specified action URL without passing through Instatic's challenge gate.