Creating dynamic forms that users can design at runtime—dragging fields, configuring validation, and previewing behaviour—gives product teams huge flexibility. Such a system lets business users create surveys, work orders, onboarding flows, or admin screens without developer changes. In this article you will learn a practical, production-ready approach to build a fully dynamic form builder using Angular (frontend), ASP.NET Core (backend), and SQL Server (metadata store). The guide covers architecture, data model, UX patterns, example code, and operational best practices.
Target audience: beginner → expert web developers (especially full-stack .NET + Angular engineers).
What is a Dynamic Form Builder?
A dynamic form builder provides:
A drag-and-drop canvas to add fields (text, number, date, select, radio, checkbox, file, custom components).
A field property editor to set label, placeholder, default value, required, validation rules, help text, conditional visibility.
A validation rule engine (regex, min/max, cross-field rules).
A preview mode to test behaviour before publishing.
A runtime renderer that converts saved form definitions into working forms.
APIs to save form definitions and collect submissions.
High-level architecture
User (Designer) <-> Angular Form Builder UI <-> ASP.NET Core API <-> SQL Server (Form + Field metadata)
|
v
Submission Store / Blob (files)
Key components
Angular Builder: drag/drop canvas, property panel, preview.
Form Renderer (Angular): runtime component that builds reactive forms from metadata.
Backend API (ASP.NET Core): store/retrieve form definitions, validation evaluation, submission endpoint.
SQL Server: canonical store for forms, fields, rules, versions.
Optional: Redis cache for published forms, blob storage for file uploads, message queue for submission processing.
Flowchart
Designer opens Builder
↓
Drag & drop fields on canvas
↓
Configure field properties + validation
↓
Preview form (client-side)
↓
Publish → API saves form JSON + version
↓
Users submit runtime form → API validates & stores submission
Workflow (smaller header)
Design -> Preview -> Publish -> Render at runtime -> Submit -> Validate -> Store
ER diagram (smaller header)
+-------------------+ +----------------------+ +----------------------+
| Forms | 1 - *| Fields | 1 - *| ValidationRules |
+-------------------+ +----------------------+ +----------------------+
| FormId (PK) | | FieldId (PK) | | RuleId (PK) |
| Name | | FormId (FK) | | FieldId (FK) |
| Version | | Type (text, select) | | RuleType (regex,...) |
| IsPublished | | Label | | Expression |
| CreatedOn | | Key (jsonKey) | | ErrorMessage |
+-------------------+ | Options (JSON) | +----------------------+
| Order |
| Conditional (JSON) |
+----------------------+
+----------------------+
| Submissions |
+----------------------+
| SubmissionId (PK) |
| FormId (FK) |
| Data (JSON) |
| CreatedOn |
+----------------------+
Architecture diagram (Visio-style, smaller header)
[ Angular Builder ] <---HTTP---> [ ASP.NET Core API ] <---EF Core---> [ SQL Server ]
| (Drag/Drop) | |
| (Preview) | |
v v v
[ Angular Renderer ] [ Validation Engine ] [ Blob Storage (files) ]
| (runtime form) | |
v v v
[ User Submits ] --------------------> [ Submission API ] --------------> [ Submissions Table ]
Sequence diagram (smaller header)
Designer -> AngularBuilder: open form
AngularBuilder -> User: shows canvas
Designer -> AngularBuilder: add field, set props
AngularBuilder -> PreviewComponent: render preview
Designer -> AngularBuilder: Publish
AngularBuilder -> API: POST /forms
API -> SQLServer: save form + version
User -> AngularRenderer: GET /forms/{id}
AngularRenderer -> API: GET form definition
API -> SQLServer: fetch form JSON
AngularRenderer -> User: render form
User -> AngularRenderer: submit form
AngularRenderer -> API: POST /forms/{id}/submit
API -> ValidationEngine: validate submission
ValidationEngine -> API: returns result
API -> SQLServer: store submission
API -> AngularRenderer: return success/fail
Data model and JSON format
Store a form as JSON in DB (plus normalized fields if needed). Example simplified JSON:
{
"formId": "guid",
"name": "Work Order",
"version": 3,
"fields": [
{
"id": "f1",
"key": "customerName",
"type": "text",
"label": "Customer Name",
"placeholder": "Enter full name",
"required": true,
"order": 1
},
{
"id": "f2",
"key": "priority",
"type": "select",
"label": "Priority",
"options": ["Low","Medium","High"],
"required": true,
"order": 2
},
{
"id": "f3",
"key": "dueDate",
"type": "date",
"label": "Due Date",
"required": false,
"conditional": {
"showIf": { "priority": "High" }
}
}
]
}
Keep ValidationRules either embedded in field JSON or as a separate table for complex rules and versioning.
Frontend: Angular builder and renderer
Libraries & concepts
Use Angular CDK DragDrop for drag-drop canvas.
Use Reactive Forms (FormGroup, FormControl) for runtime form building.
Use a component library (Material, PrimeNG) for neat UI.
Keep a Field Registry mapping type -> component so you can add custom field types later.
Builder: key parts
Palette: list of field types to drag.
Canvas: droppable container that maintains an ordered array of field metadata.
Property Panel: edit selected field’s properties (label, placeholder, validations, options).
Preview: client-side runtime that converts current metadata to a FormGroup and shows it.
Example: DragDrop + saving metadata (simplified)
// builder.component.ts (simplified)
fields: FormField[] = [];
drop(event: CdkDragDrop<FormField[]>) {
moveItemInArray(this.fields, event.previousIndex, event.currentIndex);
}
addField(type: string) {
const newField: FormField = {
id: uuid(),
key: 'field_' + Date.now(),
type,
label: 'New ' + type,
required: false,
order: this.fields.length + 1
};
this.fields.push(newField);
}
save() {
const payload = { name: this.formName, fields: this.fields };
this.http.post('/api/forms', payload).subscribe();
}
Renderer: build reactive form from metadata
buildForm(fields: FormField[]) {
const group: { [k: string]: FormControl } = {};
fields.forEach(f => {
const validators = [];
if (f.required) validators.push(Validators.required);
if (f.pattern) validators.push(Validators.pattern(f.pattern));
if (f.min != null) validators.push(Validators.min(f.min));
group[f.key] = new FormControl(f.default || null, validators);
});
this.form = new FormGroup(group);
}
For conditional visibility, use valueChanges subscriptions to show/hide controls and to add/remove validators dynamically.
Backend: ASP.NET Core design
API endpoints (suggested)
POST /api/forms — create form (draft).
PUT /api/forms/{id} — update draft.
POST /api/forms/{id}/publish — publish version.
GET /api/forms/{id} — get published form definition.
POST /api/forms/{id}/submit — submit filled data.
GET /api/forms/{id}/submissions — list submissions.
Persistence
Save complete JSON in Forms table (FormId, Name, Version, IsPublished, FormJson).
Optionally normalize fields in Fields table to support search/analytics.
Save submissions in Submissions table with Data JSON column for flexible schema.
Server-side validation
Always validate submissions server-side using the same rules defined in metadata. Implement a ValidationEngine that:
Reads form definition (from cache or DB).
Validates each field against type, required, regex, min/max.
Executes cross-field rules and conditional checks.
Returns structured validation errors to client.
Example validation pseudo
foreach (var field in form.Fields) {
var value = submission.Data[field.Key];
if (field.Required && value is null) AddError(field.Key, "Required");
if (field.Pattern != null && !Regex.IsMatch(value, field.Pattern)) AddError(field.Key, "Invalid format");
// cross-field: if A == X then B required...
}
Validation rules — simple to advanced
Simple rules: required, min, max, pattern.
Conditional rules: showIf, enableIf. Represent as small JSON expressions: { "showIf": { "status": "Approved" } }.
Cross-field rules: e.g., if quantity > 0 then price > 0. Store as expressions or a tiny DSL. Evaluate with a safe expression engine (no arbitrary code execution). Use a library like NCalc (C#) or implement a safe evaluator.
Custom validators: allow plugin validators (server-side functions) for complex checks (duplicate check, DB lookup).
Versioning and publishing
Keep each published form as immutable versioned JSON.
Builder edits a draft (IsPublished = false). Publishing clones to a new version with IsPublished = true and increment version number.
Runtime renderer always references a specific published version to avoid breaking changes for in-progress submissions.
File uploads & large data
For file fields, use pre-signed URLs (Azure SAS, AWS S3 pre-signed) returned by API to upload directly to blob storage. Store file URL in submission JSON. This avoids storing large blobs in SQL Server and reduces server load.
Caching & performance
Cache published form definitions (Redis or in-memory) for fast read (forms are read-heavy).
Cache validation metadata too.
Store compiled validation expressions where possible to avoid parsing at runtime.
Security considerations
Only authenticated users should access builder or publish forms. Use RBAC for who can design, publish, and view submissions.
Sanitize and limit any custom expressions to avoid code injection.
Validate all submissions on server side.
Limit file upload types and sizes and scan for malware if required.
Rate limit submission endpoints to protect against abuse.
UX tips for builder and preview
Allow undo/redo for designer operations.
Show field placeholders and sample data in preview.
Provide templates for common forms (signup, order, feedback).
Provide a JSON export/import feature for forms.
Provide "test data" mode for designers to simulate submissions.
Show validation errors in preview exactly as users will see them.
Analytics and monitoring
Track form usage: views, average completion time, abandonment rate, field-level dropoffs.
Track validation failures frequency to detect poorly worded fields.
Build dashboards (Power BI / Grafana) that query Submissions and Forms metadata.
Example: Minimal EF Core models
public class Form {
public Guid FormId { get; set; }
public string Name { get; set; }
public int Version { get; set; }
public bool IsPublished { get; set; }
public string FormJson { get; set; } // JSON columns
public DateTime CreatedOn { get; set; }
}
public class Submission {
public Guid SubmissionId { get; set; }
public Guid FormId { get; set; }
public string DataJson { get; set; }
public DateTime CreatedOn { get; set; }
}
Use System.Text.Json for fast JSON (or Newtonsoft.Json if needed).
Testing strategy
Unit test ValidationEngine for many rule combinations.
E2E tests for builder → publish → render → submit cycle.
Load test submission endpoint for concurrent writes.
Security tests for custom expressions and file uploads.
Roadmap: advanced features
Form branching & multi-page flows: allow conditional navigation and multi-step forms.
Workflow integrations: on submit, trigger actions (email, create ticket, call webhook).
Permissioned fields: field visibility per role.
Localization: support multi-language labels and option sets.
Form templating & inheritance: compose forms from reusable sections.
Audit trail: who changed what in form definition (for compliance).
Conclusion
A fully dynamic form builder is a very useful product feature for enterprise applications. With Angular CDK drag-drop, reactive forms, a robust backend validation engine, and a flexible JSON metadata model in SQL Server, you can deliver a builder that is both powerful and safe. Key points to remember:
Keep form definitions versioned and immutable when published.
Implement server-side validation mirroring client rules.
Use safe expression evaluators for conditional and cross-field rules.
Cache published forms for runtime performance.
Provide preview and testing tools for designers.