Build and save a form
You know how route files work — loader reads, action writes, component renders. Time to put that into practice. Let's build the page where you create forms.
Build the form creator
Ask your AI:
Build all the pages for my form builder. Use the existing `Form` and `Submission` models from `app/models/.server/` for all database operations. (1) `/forms/new` — a page where I can type a title, add fields (each field has a label and a type — text, email, or textarea), and click Save. When I save, use `validate(formData, schema)` from `~/lib/data/validate` with the existing insert schema from the schema file to validate the input, then call `Form.create()` to write it to the database with a randomly generated token, then redirect me to the results page. If validation fails, `return { errors }` (no special status code) so the form can display them. (2) `/forms/$id` — the public form page. Call `Form.findByID()` to load the form, render the fields, and `Submission.create()` to save it when someone fills it out. (3) `/forms/$id/results/$token` — the results page. Call `Form.findBy()` to verify the token matches, then show the form title, a link to the public form URL, and a list of submissions. If the token is wrong, show an error. Import the React Router Form component as `RouterForm` to avoid clashing with the Form model. Also replace the "Next steps" section on the home page with a prominent link to `/forms/new`.
The AI should use your models and validation schemas. If it writes raw
db.insert() or db.query instead, tell it: "Use the Form and Submission
models from app/models/.server/ and use validate() from
~/lib/data/validate with the existing insert schema before creating."
The AI will create a builder page with inputs for the title and fields, plus a save button that sends the form to the server — a server action. The server validates the input, writes it to the database through the model, and redirects you to the results page.
If something's off — the page doesn't load, the form doesn't save, the redirect doesn't work — tell the AI what's wrong and let it fix it. You know this loop.
Two things called Form
Your app now has two different things named Form — the model (for database operations) and the React Router component (for rendering HTML forms). JavaScript won't let you import both with the same name into the same file.
The fix is an import alias — renaming one on import:
import { Form as RouterForm } from 'react-router'
This tells JavaScript: "import Form from react-router, but call it RouterForm in this file." The model keeps its name Form since it's the core of what you're building. Your AI should handle this automatically based on the prompt — if it doesn't, tell it to alias the React Router import.
Try it out
Open /forms/new in your browser. Give your form a title — something like "Event Feedback." Add a few fields:
- "Your name" (text)
- "Your email" (email)
- "Any comments?" (textarea)
Hit Save.


You should land on the results page. It's empty — no one has submitted yet — but you can see the public form URL. This is your admin page — you can always get back to it with the browser's back button.
If you're curious, run pnpm drizzle-kit studio and open local.drizzle.studio — Drizzle Studio will show your form sitting in the forms table.
How actions respond
Open the new.tsx route file your AI created. When the action runs, two things can happen:
- Success —
redirect()sends the user to the results page, like/forms/${form.id}/results/${form.token}. The form is saved, the user sees their new form. - Validation failure —
return { errors }sends the error messages back. The form re-renders with the errors shown next to the relevant fields. Nothing is saved.
This pattern — redirect on success, return errors on failure — is the same in every route that handles form submissions.
The link is the key
Notice that the results URL has a long random string in it — that's the secret that only lets you in. Anyone with this URL can see responses. Anyone without it can't. No login needed. The link itself is the permission.
This is called a capability URL — a real pattern used by Google Docs ("anyone with the link") and Doodle polls. Simple, effective, and no accounts to manage.
?What is a capability URL?