Tasks
Build a form
Define a database-stored form in code, seed it, and publish it at /forms/<slug> with email delivery.
What you'll build: a published contact form at /forms/contact. You define it in code and seed it into the
database. It stores each submission and emails it to your team.
Prerequisites
- The workspace starter, or another host app with users and the admin area.
- Forms, added as in Add a module, with its encryption key, permissions and migration.
The paths below are for the starter's apps/web.
Add a module installs Forms, enables NuxtHub KV and blob storage, sets
its encryption key, gives its permissions to a role and generates the migration. Forms uses KV for rate limiting and blob storage for
uploads. It installs Notify for its emails.
1. Write the definition
A form defined in code is versioned with your app, and the same in every environment. defineForm() from
@frigear-nu/forms/utils types the definition as a FormDefinition: a mode, steps, fields, and where the answers go.
apps/web/forms/contact.ts
import { defineForm } from '@frigear-nu/forms/utils'
export const contactForm = defineForm({
mode: 'single',
storage: 'db',
steps: [
{
id: 'message',
fields: [
{ key: 'name', type: 'text', label: 'Name', required: true },
{ key: 'email', type: 'email', label: 'Email', required: true },
{
key: 'subject',
type: 'select',
label: 'Subject',
options: [{ value: 'support', label: 'Support' }, { value: 'other', label: 'Other' }],
},
{ key: 'message', type: 'textarea', label: 'Message', required: true },
],
},
],
submit: { successMessage: 'Thanks, we will be in touch.' },
delivery: {
email: {
to: ['office@example.com'],
subject: 'New message from {{ answer.name }}',
replyToField: 'email',
},
},
})
- Recipients.
totakes literal addresses. Without abody, the email lists every visible answer. - Field keys. They start with a letter, and contain letters, digits and underscores.
- Reply-to.
replyToFieldmust name anemailfield. - Access.
accessdefaults to{ availability: 'web', auth: 'none' }: a public page with no sign-in. - Delivery. The email goes through Notify. Notify prints it with the
logdriver until you configure a real one.
The submission is stored before the email is sent. So a delivery error never loses a submission.
2. Enable the pages and seed the form
Seeding puts the form in the database on start, so you need not create it by hand in the admin area. Add the pages and
the seed to the forms options you set in Add a module:
apps/web/nuxt.config.ts
import { contactForm } from './forms/contact'
export default defineNuxtConfig({
forms: {
settings: {
pages: { form: true, admin: true, adminForm: true, adminSubmissions: true, adminSubmission: true },
},
seed: {
enabled: true, // or NUXT_FORMS_SEED_ENABLED=true
forms: [{ slug: 'contact', name: 'Contact', status: 'published', definition: contactForm }],
},
},
})
statusdefaults todraft.- Seeding inserts a slug only when it does not exist yet. It never overwrites a form.
After the first seed, edit the form in the admin area, at
/admin/forms. A changed definition in code does not reach
a form that is already seeded.3. Open the form
Restart pnpm dev. The seed runs on start.
To place the form inside any page instead, render <FormRenderer slug="contact" />.
What you should see: the form at
/forms/contact. After you submit it, the submission is under /admin/forms,
and the email is printed in the terminal.Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
/admin/forms refuses you | None of your roles grants admin:access and forms:manage. | Grant them, as in Add a module. |
/forms/contact is not found | The seed is off, or the form is a draft. | Set seed.enabled: true and status: 'published'. |
| No email arrives | Notify still uses the log driver. | Configure a driver, as in Send an email. |
Next steps
- Forms – add conditional fields (
showIf), steps, file uploads, captcha, open periods and access tokens. - Send an email – send email through a real provider instead of the terminal.
- Frigear website app – see an app that seeds three forms and polices them with its own policy.