Forms reference
This page lists what Forms adds to your server: hooks, routes, tables and translations. It is for app developers and module authors. Set up the module first: Overview.
Hooks
Your app answers these Nitro hooks from a server plugin (server/plugins/*.ts).
| Hook | When | Payload |
|---|---|---|
forms:submitted | A valid submission arrived, and was stored unless storage: 'none'. | SubmittedContext |
forms:captcha:verify | A captcha token must be verified. | CaptchaVerifyContext |
forms:delivered | Delivery finished: a listener delivered the submission, or one failed. | { submissionId: string | null, formSlug, status: 'delivered' | 'failed', error? } |
type SubmittedContext = {
event: H3Event
form: { id: string, slug: string, name: string, version: number, definition: FormDefinition }
answers: Record<string, unknown> // validated; file fields hold the file names
files: { fieldKey: string, filename: string, contentType: string, size: number, pathname: string | null }[]
locale: string
userId: number | null
submissionId: string | null // null when storage is 'none'
stored: boolean
delivered: boolean // set true once the handler delivered; throw to mark delivery failed
}
type CaptchaVerifyContext = {
event: H3Event
provider: 'turnstile' | 'recaptcha' | 'hcaptcha'
token: string
ip: string | undefined
formId?: string
secretKey?: string
secretSource?: 'form' | 'env' | 'unset'
valid: boolean | undefined // set it to decide; left undefined, the provider is asked
}
This listener forwards every submission to a webhook:
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('forms:submitted', async (ctx) => {
await fetch('https://example.com/hook', { method: 'POST', body: JSON.stringify(ctx.answers) })
ctx.delivered = true
})
})
- Email. The built-in email delivery is a
forms:submittedlistener. It sendsdelivery.emailthrough Notify. - Stored forms. With
storage: 'db', a failing listener sets the submission'sdeliveryStatustofailed, with the error. The visitor still sees success, and Deliver again in the admin area runs the hooks again. - Unstored forms. With
storage: 'none', nothing else keeps the answers, so a failure answers502.
Routes
Routes the module registers, and who may call them. Public routes are under /api/forms, and staff routes under
/api/admin/forms. Build the paths with formsApi from @frigear-nu/forms/api, for example
formsApi.admin.form(id).
- Staff means the caller holds
forms:managethrough a role. See Permissions. - Session routes create or remove access credentials. They need
forms:credentials, a session-only permission, so a token gets403 session_required. - Errors carry a
data.code, such asnot_found,forbidden,conflictorvalidation_failed. - Lists answer
{ items, total, page, pageSize }. They takepage,pageSize(default 20, at most 100),sort,order(ascordesc) and their filters.
| Method | Path | Auth | Status | Purpose |
|---|---|---|---|---|
GET | /api/forms/:slug | form access | 200 | The published definition, without delivery. |
GET | /api/forms/:slug/access | none | 200 | What a page needs to choose between the form, a gate and "not found". |
POST | /api/forms/:slug/submit | form access | 200 | Submit answers. Rate limited. |
GET | /api/admin/forms | forms:manage | 200 | Forms with submission counts. Filters below. |
POST | /api/admin/forms | forms:manage | 201 | Create a form. 409 conflict for a taken slug. |
POST | /api/admin/forms/import | forms:manage | 200 | Import definitions: { inserted, skipped }. |
GET | /api/admin/forms/:id | forms:manage | 200 | One form, with submissionCount. |
PATCH | /api/admin/forms/:id | forms:manage | 200 | Update a form. A changed definition bumps version. |
DELETE | /api/admin/forms/:id | forms:manage | 204 | Archive the form. It and its submissions are kept. |
POST | /api/admin/forms/:id/clone | forms:manage | 201 | Copy into a new draft (<slug>-copy, -copy-2, …). |
GET | /api/admin/forms/:id/submissions | forms:manage | 200 | A form's submissions. Filters below. |
GET | /api/admin/forms/:id/tokens | forms:manage | 200 | The form access tokens, as an array. |
POST | /api/admin/forms/:id/tokens | forms:credentials | 201 | Create a token. Only this response contains it. |
DELETE | /api/admin/forms/:id/tokens/:tokenId | forms:credentials | 204 | Revoke a token. It stays listed, with revokedAt. |
GET | /api/admin/forms/:id/password | forms:manage | 200 | Whether a password is set: { set }. |
PUT | /api/admin/forms/:id/password | forms:credentials | 200 | Set the password { password }, 6 to 200 characters. Answers { set: true }. |
DELETE | /api/admin/forms/:id/password | forms:credentials | 204 | Remove the password. |
GET | /api/admin/forms/:id/secrets | forms:manage | 200 | The names and sources of the secrets: { keys, captcha, unreadable }. |
PUT | /api/admin/forms/:id/secrets/:key | forms:credentials | 200 | Write one secret { value }. 400 for an unknown key. |
DELETE | /api/admin/forms/:id/secrets/:key | forms:credentials | 204 | Remove one secret. 400 for an unknown key. |
GET | /api/admin/forms/submissions/:id | forms:manage | 200 | One submission. |
GET | /api/admin/forms/submissions/:id/files/:fileId | forms:manage | 200 | Download an uploaded file. |
POST | /api/admin/forms/submissions/:id/redeliver | forms:manage | 200 | Run the delivery hooks again: { deliveryStatus, deliveryError }. |
GET /api/admin/forms filters by q (name or slug), status and archived=false. It sorts by updatedAt
(the default, descending), createdAt, name, slug, status or submissionCount.
DELETE /api/admin/forms/:id sets the status to archived, so submissions always keep their form. A PATCH can
publish it again.
POST /api/admin/forms/:id/clone copies the name and definition. Submissions, tokens, the password and secrets
are not copied.
GET /api/admin/forms/:id/submissions filters by deliveryStatus. It sorts by createdAt (the default,
descending), deliveryStatus or locale.
The static import and submissions segments win over :id.
Database
Tables the module adds. Generate and commit a migration after installing.
| Table | Holds |
|---|---|
forms | slug, name, status (draft, published, archived), definition and version. |
form_submissions | Answers, definitionSnapshot, formVersion, userId, locale, deliveryStatus and deliveryError. |
form_submission_files | The blob pathname, file name, content type and size of each upload. |
form_access_tokens | Form access tokens: tokenHash, prefix, lastUsedAt and revokedAt. |
form_access | One row per password-protected form: passwordHash. |
form_secrets | One row per form: secrets, an encrypted JSON map of namespaced keys. |
deliveryStatus is none, pending, delivered or failed. Only staff can download uploaded files.
Translations
Override any string in your app's i18n/locales/<code>.json. The module registers da and en messages under
forms.*, with English fallbacks. See
overriding module strings.
{ "forms": { "actions": { "submit": "Send inquiry" } } }
Key groups: actions, step, success, error, embed, preview, field, value, status, delivery,
validation, admin, schedule, access, menu, captcha and builder.
- Form content lives in the definition, as
{ da, en }texts. - Validation messages use
forms.validation.*. A field overrides them withvalidation.message.
Next steps
- Server API – the access modes and secrets behind these routes.
- Send an email – configure the Notify driver that delivers form emails.
- Definition – the
definitionthese routes store.