Skip to main content
Every published form gets a unique, shareable URL where users can submit responses. The system handles validation, data collection, and success messaging automatically.

Public Form URLs

When you publish a form, it becomes accessible at a slug-based URL:
https://yourapp.com/f/{slug}

Slug Generation

Each form is assigned a unique slug on creation:
  • Auto-generated by Prisma
  • URL-safe characters only
  • Guaranteed unique across all forms
  • Persists for the lifetime of the form
Slugs are stored in the database with a unique constraint, preventing collisions even at scale.

Form Rendering

The public form page dynamically renders fields based on the stored schema.

Page Load Flow

1

Extract slug from URL

Next.js dynamic routing captures the slug from the URL path.
// app/f/[slug]/page.tsx:145-146
const params = useParams();
const slug = params.slug as string;
2

Fetch form data

The component fetches form configuration from the API using the slug.
// app/f/[slug]/page.tsx:148-161
const fetchFormData = async () => {
  try {
    const response = await axios.get(`/api/forms/${slug}`);
    const { data } = response.data;
    setFormData(data);
    setIsLoading(false);
  } catch (error: any) {
    setError(error.message);
    setIsLoading(false);
  }
};
fetchFormData();
3

Render form fields

Fields are dynamically rendered based on their type using the renderField function.
// app/f/[slug]/page.tsx:219-229
<form onSubmit={handleSubmit} className="space-y-6">
  {formData.fields.map((field: Field) => (
    <div key={field.id}>
      <label className="block mb-2 font-medium">
        {field.label}
        {field.required && <span className="text-red-500"> *</span>}
      </label>
      {renderField(field, formValues[field.id], handleFieldChange)}
    </div>
  ))}
</form>

Field Rendering

Each field type has specialized rendering logic:
// app/f/[slug]/page.tsx:32-44
case "text":
case "email":
  return (
    <input
      type={field.type}
      name={field.id}
      placeholder={field.placeholder}
      required={field.required}
      value={value || ""}
      onChange={(e) => onChange(field.id, e.target.value)}
      className="w-full border rounded px-3 py-2"
    />
  );

Form State Management

Form values are tracked in React state:
// app/f/[slug]/page.tsx:141
const [formValues, setFormValues] = useState<Record<string, any>>({});

Value Updates

Each field change updates the centralized state object:
// app/f/[slug]/page.tsx:163-168
const handleFieldChange = (fieldId: string, value: any) => {
  setFormValues((prev) => ({
    ...prev,
    [fieldId]: value,
  }));
};
Using a single state object with field IDs as keys makes it easy to construct the submission payload.

Form Submission

When the user clicks Submit, the form data is validated and sent to the API.
1

User clicks Submit

The form’s onSubmit handler is triggered.
2

Browser validation

HTML5 validation runs automatically for required fields and input types (email, number, etc.).
3

Prevent default & submit

The handler prevents default form behavior and sends an API request.
// app/f/[slug]/page.tsx:170-188
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setIsSubmitting(true);

  try {
    await axios.post("/api/forms/submit", {
      formId: formData?.id,
      data: formValues,
    });

    setSubmitSuccess(true);
    setFormValues({});
  } catch (error: any) {
    console.error("Form submission failed:", error);
    setError("Failed to submit form. Please try again.");
  } finally {
    setIsSubmitting(false);
  }
};
4

Server-side validation

The API validates the submission (see next section).
5

Success message

On success, a thank you message is displayed.
// app/f/[slug]/page.tsx:202-213
if (submitSuccess) {
  return (
    <div className="min-h-screen p-8 max-w-xl mx-auto flex items-center justify-center">
      <div className="text-center">
        <h2 className="text-2xl font-bold mb-4">Thank you!</h2>
        <p className="text-gray-600">
          Your response has been submitted successfully.
        </p>
      </div>
    </div>
  );
}

Validation

Multiple layers of validation ensure data integrity.

Client-Side Validation

HTML5 validation runs automatically:
  • required attribute for mandatory fields
  • type="email" for email format
  • type="number" for numeric values
// app/f/[slug]/page.tsx:36
required={field.required}

Server-Side Validation

The API performs comprehensive validation before storing responses.
1

Check form ID

// app/api/forms/submit/route.ts:11-16
if (!formId || formId.trim().length === 0) {
  return NextResponse.json(
    { error: "Form ID is required and cannot be empty" },
    { status: 400 },
  );
}
2

Validate data object

// app/api/forms/submit/route.ts:18-23
if (!data || typeof data !== "object") {
  return NextResponse.json(
    { error: "Form data is required and must be an object" },
    { status: 400 },
  );
}
3

Verify form exists

// app/api/forms/submit/route.ts:25-31
const form = await prisma.forms.findUnique({
  where: { id: formId },
});

if (!form) {
  return NextResponse.json({ error: "Form not found" }, { status: 404 });
}
4

Validate required fields

// app/api/forms/submit/route.ts:42-49
for (const field of formFields) {
  if (field.required && !submittedKeys.includes(field.id)) {
    return NextResponse.json(
      { error: `Required field missing: ${field.label}` },
      { status: 400 },
    );
  }
}
5

Store response

// app/api/forms/submit/route.ts:51-56
const formResponse = await prisma.formsResponses.create({
  data: {
    formId: formId,
    data: data,
  },
});
The API returns specific error messages for validation failures, helping users understand what went wrong.

Response Storage

Responses are stored in the FormsResponses table:
model FormsResponses {
  id        String   @id @default(cuid())
  formId    String
  data      Json     // User's submitted data
  createdAt DateTime @default(now())
  form      Forms    @relation(fields: [formId], references: [id], onDelete: Cascade)
}

Key Features

The data field stores the entire submission as JSON, allowing flexible schemas without migrations.Example stored data:
{
  "full_name": "John Doe",
  "email": "john@example.com",
  "rating": "Very Satisfied",
  "comments": "Great service!"
}
When a form is deleted, all its responses are automatically removed via onDelete: Cascade.
createdAt automatically records when each response was submitted for analytics and exports.

Error States

The public form page handles various error scenarios:
if (isLoading) {
  return <div className="p-8">Loading form...</div>;
}

UX Features

Loading States

The submit button shows loading state during submission:
// app/f/[slug]/page.tsx:231-237
<button
  type="submit"
  disabled={isSubmitting}
  className="w-full mt-4 px-4 py-2 bg-black text-white rounded hover:bg-gray-800 disabled:opacity-50"
>
  {isSubmitting ? "Submitting..." : "Submit"}
</button>

Required Field Indicators

Required fields are clearly marked with a red asterisk:
// app/f/[slug]/page.tsx:222-225
<label className="block mb-2 font-medium">
  {field.label}
  {field.required && <span className="text-red-500"> *</span>}
</label>
The combination of visual indicators and browser validation provides clear feedback before users submit.

API Endpoint

POST /api/forms/submit Request Body:
{
  "formId": "cm123abc",
  "data": {
    "full_name": "John Doe",
    "email": "john@example.com",
    "rating": "Very Satisfied"
  }
}
Success Response:
{
  "success": true,
  "message": "Response submitted successfully",
  "responseId": "cm456def"
}
Error Response:
{
  "error": "Required field missing: Email Address"
}
Implementation: app/api/forms/submit/route.ts