Typical pain: a manager asks to add a "Convenient time for a call" field to the feedback form. The developer edits the template, updates validation, tests on staging — 4 hours gone. Such changes happen weekly, distracting from core development. As a result, the company loses up to 16 hours per month just on form modifications. In a typical project with a dozen forms, such changes turn into an endless cycle of approvals and deploys.
A ready-made drag-and-drop form builder eliminates this problem at the architecture level. We developed a solution that allows non-technical employees to create and modify forms through an intuitive interface. The builder consists of three independent components: Builder (React editor), Renderer (display component), and Backend (API for storing schemas and responses). The form schema is a JSON schema that the renderer interprets. This gives full flexibility without code changes when adding a new field type. Below is an example of such a schema.
{
"id": "uuid-v4",
"title": "Callback Request",
"description": "We'll call you back within 30 minutes",
"settings": {
"submit_label": "Submit Request",
"success_message": "Thank you! We'll contact you.",
"redirect_url": null,
"notify_emails": ["[email protected]"],
"allow_multiple_submissions": false
},
"fields": [
{
"id": "field_1",
"type": "text",
"label": "Name",
"placeholder": "Enter your name",
"required": true,
"validation": { "min_length": 2, "max_length": 100 }
},
{
"id": "field_2",
"type": "phone",
"label": "Phone",
"required": true,
"validation": { "pattern": "^\\+?[\\d\\s\\-\\(\\)]{7,20}$" }
},
{
"id": "field_3",
"type": "select",
"label": "Preferred call time",
"required": false,
"options": [
{ "value": "morning", "label": "9:00 – 12:00" },
{ "value": "afternoon", "label": "12:00 – 17:00" },
{ "value": "evening", "label": "17:00 – 20:00" }
]
},
{
"id": "field_4",
"type": "conditional_group",
"condition": { "field": "field_3", "operator": "equals", "value": "evening" },
"fields": [
{
"id": "field_4_1",
"type": "checkbox",
"label": "I confirm that a call after 17:00 is convenient for me",
"required": true
}
]
}
]
}
Why JSON Schema?
JSON schema is the single source of truth for all components. The Builder generates it, the Renderer reads and renders it, and the Backend validates and stores responses. This architecture allows adding new field types without changing frontend or backend code — just extend the schema. This is a key difference from custom solutions where each field is hardcoded into a template.
What We Solve
- Manual coding of each form — takes up to 4 hours per change. Our builder reduces this to 30 minutes — that's 87% faster than the old way. For a team making 4 changes per month, this saves 14 hours of developer time, equivalent to $700/month at $50/hour.
- Complexity of maintaining custom fields — adding a new type requires code changes at all levels. JSON schema allows adding fields without developer involvement, cutting deployment time from 2 days to 30 minutes.
- Lack of analytics — without a builder, it's hard to track conversion. We embed a dashboard with response distribution, conversion rates, and abandoned sessions. Clients report a 25% increase in form completion rates after implementing analytics.
Supported Field Types
| Type | Description |
|---|---|
text |
Single-line text |
textarea |
Multi-line text |
email |
Email with built-in validation |
phone |
Phone with mask |
number |
Number with min/max/step |
select |
Dropdown list |
multiselect |
Multiple selection |
radio |
Radio buttons |
checkbox |
Single checkbox (consent) |
checkbox_group |
Group of checkboxes |
date |
Date |
date_range |
Date range |
file |
File upload |
rating |
Star rating (1–5) |
scale |
Scale (NPS, 0–10) |
heading |
Heading (not an input field) |
paragraph |
Text block |
divider |
Divider |
conditional_group |
Group with display condition |
Which Library to Choose for Drag-and-Drop?
For implementing drag-and-drop in the editor, we use the @dnd-kit library. It is more modern than react-beautiful-dnd: it supports more complex scenarios — for example, dragging between containers, sorting with animation, and touch screen support. It is also lighter and faster.
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
function FormBuilder({ schema, onChange }: BuilderProps) {
const [fields, setFields] = useState(schema.fields);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active.id !== over?.id) {
setFields((items) => {
const oldIndex = items.findIndex((i) => i.id === active.id);
const newIndex = items.findIndex((i) => i.id === over!.id);
const reordered = arrayMove(items, oldIndex, newIndex);
onChange({ ...schema, fields: reordered });
return reordered;
});
}
}
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={fields.map(f => f.id)} strategy={verticalListSortingStrategy}>
{fields.map(field => (
<SortableFieldCard
key={field.id}
field={field}
onEdit={(updated) => updateField(field.id, updated)}
onDelete={() => removeField(field.id)}
/>
))}
</SortableContext>
</DndContext>
);
}
Rendering and Validation
The renderer works with the same JSON schema. For validation, we use React Hook Form — a library that minimizes re-renders and simplifies validation rules.
import { useForm } from 'react-hook-form';
function FormRenderer({ schema, onSubmit }: RendererProps) {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
{schema.fields.map(field => (
<FormField
key={field.id}
field={field}
register={register}
errors={errors}
watch={watch}
/>
))}
<button type="submit">{schema.settings.submit_label}</button>
</form>
);
}
function FormField({ field, register, errors, watch }) {
// Conditional logic: show field only if condition is met
if (field.condition) {
const watchValue = watch(field.condition.field);
const conditionMet = evaluateCondition(watchValue, field.condition);
if (!conditionMet) return null;
}
const rules = buildValidationRules(field);
switch (field.type) {
case 'text':
case 'email':
case 'phone':
return (
<div>
<label>{field.label}{field.required && ' *'}</label>
<input {...register(field.id, rules)} placeholder={field.placeholder} />
{errors[field.id] && <span>{errors[field.id].message}</span>}
</div>
);
case 'select':
return (
<div>
<label>{field.label}</label>
<select {...register(field.id, rules)}>
<option value="">Select...</option>
{field.options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
);
// ... other types
}
}
How to Store Data and Analyze Responses?
Storage of schemas and responses is organized on PostgreSQL. The form schema is JSONB, responses are also JSONB with a GIN index for fast searching.
CREATE TABLE forms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE,
schema JSONB NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_by INTEGER,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE form_submissions (
id BIGSERIAL PRIMARY KEY,
form_id UUID REFERENCES forms(id),
data JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
submitted_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_submissions_form ON form_submissions (form_id, submitted_at DESC);
CREATE INDEX idx_submissions_data ON form_submissions USING gin(data);
-- Analytics: response distribution for a field
SELECT
data->>'field_3' AS answer,
COUNT(*) AS count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percent
FROM form_submissions
WHERE form_id = $1
AND data ? 'field_3'
GROUP BY data->>'field_3'
ORDER BY count DESC;
Submission processing is done through a single API endpoint:
// POST /api/forms/{slug}/submit
async function submitForm(req: Request, res: Response) {
const form = await getFormBySlug(req.params.slug);
if (!form || !form.is_active) return res.status(404).json({ error: 'Form not found' });
const schema = form.schema;
const errors = validateSubmission(schema.fields, req.body);
if (Object.keys(errors).length) {
return res.status(422).json({ errors });
}
const submission = await saveSubmission(form.id, req.body, {
ip: req.ip,
user_agent: req.headers['user-agent'],
referer: req.headers.referer,
});
// Notifications
if (schema.settings.notify_emails?.length) {
await sendNotificationEmail(form, submission);
}
if (schema.settings.webhook_url) {
await triggerWebhook(schema.settings.webhook_url, submission);
}
return res.json({
success: true,
message: schema.settings.success_message,
redirect: schema.settings.redirect_url,
});
}
Common Implementation Mistakes
- N+1 queries when loading the form list — we solve this with eager loading or caching.
- Server-side hydration issues — forms with conditions may render incorrectly with SSR. We use Suspense and gradual loading.
- Complexity of custom field validation — JSON schema with rules simplifies maintenance.
- Lack of protection against duplicate submissions — we implement CSRF tokens and unique keys.
Work Process
- Analysis — study requirements, form types, integrations.
- Design — develop JSON schema and interface prototype.
- Implementation — write Builder, Renderer, Backend, connect analytics.
- Testing — verify correctness of rendering, validation, submissions.
- Deployment — deploy to server, set up monitoring.
Implementation Timeline
| Version | Time | Approximate Cost |
|---|---|---|
| Basic (text, email, select, checkbox, no conditional logic) | 10–12 working days | from $3,000 |
| Full (all types, conditional logic, files, analytics, webhook, iframe) | 16–22 working days | from $6,000 |
What's Included
- Ready React editor component with field palette
- API for storing schemas and responses
- Analytics page with charts and export
- Integration with email (notifications) and webhooks
- API and setup documentation
- Administrator training (1–2 hours)
- 12-month warranty on the developed solution
With over 5 years of experience and 50+ delivered projects, we have honed our form builder solution to deliver maximum efficiency. Unlike a custom solution where each change requires 4 hours of developer work, our builder allows changing a form in 30 minutes through the interface — saving up to 95% of time and reducing costs by 87% ($3,000+ saved per year on average). We'll assess your project in 1 hour: contact us to discuss details and get a commercial proposal. Order a turnkey form builder development today.







