Scaffold a complete GOV.UK form page for a Defra Hapi service. Use when building a new form page with user input — generates the Nunjucks template with govukErrorSummary and field macros, a Joi validation schema with custom error messages, and a Hapi route handler with GET and POST methods following the GDS error pattern.
Scaffold a complete, standards-compliant GOV.UK form page. Read the requirement or existing code to determine what fields are needed and their types, then generate all artefacts together.
app/views/<name>.njk)govukErrorSummary at the top of <main>, before the <h1> — only render when errorList has items:
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
{% if errorList | length %}
{{ govukErrorSummary({ titleText: "There is a problem", errorList: errorList }) }}
{% endif %}
value: values.<name> and errorMessage: fieldErrors.<name>:
govukInputgovukTextareagovukRadios with fieldset.legendgovukCheckboxes with fieldset.legendgovukDateInput with namePrefix and fieldset.legendgovukFileUploadgovukButton({ text: "Continue" }){{ crumb }}app/schemas/<name>.schema.js)<name>Schema = joi.object({ ... }).messages() with user-facing strings for every error key:
'any.required' / 'string.empty' → "Enter [what the field is]"'string.max' → "[Field] must be [n] characters or less"'string.email' → "Enter an email address in the correct format, like [email protected]"'number.base' → "Enter a number"'date.base' → "Enter a real date"{ abortEarly: false } at call sites so all errors surface at onceapp/routes/<name>.js)<name>Routes containing two route objectsGET /path — render view with empty state: { errorList: [], fieldErrors: {}, values: {} }POST /path:
request.payload against the schema with { abortEarly: false }error.details and re-render the viewapp/helpers/form-errors.js if shared)/**
* @param {import('@hapi/joi').ValidationError} error
* @returns {{ errorList: Array<{text: string, href: string}>, fieldErrors: Record<string, {text: string}> }}
*/
export function mapValidationErrors(error) {
const errorList = []
const fieldErrors = {}
for (const detail of error.details) {
const field = String(detail.path[0])
const text = detail.message
if (!fieldErrors[field]) {
errorList.push({ text, href: `#${field}` })
fieldErrors[field] = { text }
}
}
return { errorList, fieldErrors }
}
| Input | Macro | Key props |
|---|---|---|
| Short text | govukInput | id, name, label, hint, value, errorMessage |
| Long text | govukTextarea | id, name, label, rows, value, errorMessage |
| Single choice | govukRadios | idPrefix, name, fieldset.legend, items, value, errorMessage |
| Multiple choice | govukCheckboxes | idPrefix, name, fieldset.legend, items, values, errorMessage |
| Date | govukDateInput | id, namePrefix, fieldset.legend, hint, items, errorMessage |
| File | govukFileUpload | id, name, label, errorMessage |