Getting started
Foundational knowledge
forms-engine-plugin is a hapi plugin that lets teams build GOV.UK forms using configuration and minimal code, based on GDS Design System patterns. Keep business logic in a backend/BFF API and treat the plugin as a thin presentation layer.
Prefer built-in behaviour wherever possible — it's been developed, user-tested, and accessibility-tested to a consistent standard. If you need something the plugin doesn't support, consider fixing it upstream so all teams benefit. When custom code is genuinely the right call, the plugin is designed to be extended.
Favour this priority order:
- Built-in components and page types
- Configuration-driven features (page events, templates) to integrate with backends
- Custom views, components, and controllers for niche requirements
Contributing back to forms-engine-plugin
Custom components you build may be useful to other Defra teams. See the contribution guide to share them upstream rather than keeping them in your own codebase.
Step 1: Add forms-engine-plugin as a dependency
Installation
npm install @defra/forms-engine-plugin --save
Dependencies
The following are plugin dependencies that are required to be registered with hapi:
npm install hapi-pino @hapi/crumb @hapi/yar @hapi/vision --save
- hapi-pino - Pino logger for hapi
- @hapi/crumb - CSRF crumb generation and validation
- @hapi/yar - Session manager
- @hapi/vision - Template rendering support
Additional npm dependencies that you will need are:
npm install nunjucks govuk-frontend --save
- nunjucks - templating engine used by GOV.UK design system
- govuk-frontend - code you need to build a user interface for government platforms and services
Optional dependencies
npm install @hapi/inert --save
- @hapi/inert - static file and directory handlers for serving GOV.UK assets and styles
Step 2: Decide where you want to store your forms and in what format
See form definition formats to understand your options. For simple use-cases, we recommend you use our disk-based form loader using YAML form definitions.
This will influence the services.formsService you provide when registering the plugin (see step 3 below).
Step 3: Register forms-engine-plugin as a hapi plugin
import plugin from '@defra/forms-engine-plugin'await server.register({ plugin, options: { // if applicable }})Full example:
import { join } from 'node:path'import hapi from '@hapi/hapi'import vision from '@hapi/vision'import yar from '@hapi/yar'import crumb from '@hapi/crumb'import inert from '@hapi/inert'import pino from 'hapi-pino'import nunjucks from 'nunjucks'import plugin from '@defra/forms-engine-plugin'
const server = hapi.server({ port: 3000})
// Register the dependent pluginsawait server.register(pino)await server.register(inert)await server.register(crumb)await server.register({ plugin: yar, options: { cookieOptions: { password: 'ENTER_YOUR_SESSION_COOKIE_PASSWORD_HERE' // Must be > 32 chars } }})
const paths = [join(config.get('appDir'), 'views')]
await server.register({ plugin: vision, options: { engines: { html: { compile(path, { environment }) { return (context) => nunjucks.compile(path, environment).render(context) } } }, path: paths, compileOptions: { environment: nunjucks.configure(paths) } }})
// Register the `forms-engine-plugin`await server.register({ plugin, options: { cache: 'session', // must match a session you've instantiated in your hapi server config. Also accepts a CacheService instance for advanced use-cases. /** * Options that forms-engine-plugin uses to render Nunjucks templates */ nunjucks: { baseLayoutPath: 'your-base-layout.html', // the base page layout. Usually based off https://design-system.service.gov.uk/styles/page-template/ paths // list of directories forms-engine-plugin should use to render your views. Must contain baseLayoutPath. }, /** * Services is what forms-engine-plugin uses to interact with external APIs */ services: { formsService, // where your forms should be retrieved from formSubmissionService, // handles storage of file uploads outputService // where your form should be submitted to }, /** * View context attributes made available to your pages. Returns an object containing an arbitrary set of key-value pairs. */ viewContext: async (request) => { // async can be dropped if there's no async code within const user = await userService.getUser(request.auth.credentials)
return { greeting: 'Hello', // available to render on a nunjucks page as {{ greeting }} username: user.username // available to render on a nunjucks page as {{ username }} } } }})
await server.start()Step 4: Include forms-engine-plugin's client-side assets
- Import forms-engine-plugin's styling
If you are on CDP, ensure your src/client/stylesheets/application.scss file contains:
@use "pkg:@defra/forms-engine-plugin";- Import forms-engine-plugin's Javascript
If you are on CDP, this just means ensuring your src/client/javascripts/application.js file contains:
import { initAll } from '@defra/forms-engine-plugin/shared.js'
initAll()Step 5: Environment variables
The following variable is always required:
SESSION_COOKIE_PASSWORD=your-secret-password-at-least-32-charsBlocks marked with # FEATURE: <name> are optional and can be omitted if the feature is not used.
# START FEATURE: Phase banner -- supports `https://` and `mailto:` links in the feedback linkFEEDBACK_LINK=http://test.com# END FEATURE: Phase banner
# START FEATURE: Hosted tools -- used if using Defra Forms' infrastructure for file uploadsDESIGNER_URL=http://localhost:3000SUBMISSION_URL=http://localhost:3002
# S3 bucket and URL of the CDP uploader. Bucket is owned by Defra Forms, uploader is your service's URL.UPLOADER_BUCKET_NAME=my-bucketUPLOADER_URL=http://localhost:7337# END FEATURE: Hosted tools
# START FEATURE: GOV.UK Notify -- used if using forms-engine-plugin's default GOV.UK Notify email senderNOTIFY_TEMPLATE_ID="your-gov-notify-api-key"NOTIFY_API_KEY="your-gov-notify-api-key"# END FEATURE: GOV.UK Notify
# START FEATURE: Google Analytics -- if enabled, shows a cookie banner and includes GA on the cookies/privacy policyGOOGLE_ANALYTICS_TRACKING_ID='12345'# END FEATURE: Google AnalyticsStep 6: Creating and loading a form
Forms in forms-engine-plugin are represented by a configuration object called a "form definition". The form definition can be stored in a location and format of your choosing by providing a formsService as a registration option. If you are using our 'loader' pattern as recommended in step 2, you will likely be writing YAML or JSON files in your repository.
Our examples primarily use JSON. If you are using YAML, simply convert the data structure from JSON to YAML and the examples will still work.
The configuration defines several top-level elements:
pages- the form journey, each representing a single web page with a path, title, and componentscomponents- one or more questions on a pageconditions- used to conditionally show and hide pageslists- data used in selection fields like Select, Checkboxes and Radios
To understand the full set of options available to you, consult our schema documentation. Specifically, the form definition schema.
Config
Pages
Pages are the main entity in the config. They are stored in a JSON Array with each representing a single web page. Users are progressed through the pages in turn, starting from the first page. This is called the form journey. Pages can be skipped by assigning a condition to the page, when the condition evaluates to false, the page is skipped.
{ // Each page is identified by an UUID "id": "449c053b-9201-4312-9a75-187ac1b720eb",
// A page title and a path are required "title": "What is your full name", "path": "/what-is-your-full-name",
// A reference to a condition "condition": "Condition UUID",
// A page contains a collection of components "components": [ // ... ]}Components
Components are categorised into two:
- Form components - the questions on a page
- Guidance components - non-form components like markdown and details
{ // Each page is identified by an UUID "id": "2e088e75-c6f6-4a0f-8f1f-3cee14c71e4c",
// A component type, title, name and shortDescription are all required "type": "TextField", "title": "Nickname", "name": "SyHQCH", "shortDescription": "Nickname",
// A component hint text is optional "hint": "Question hint text here",
// Different options are available per component // All components support the `required (boolean) option. "options": { "required": true }, // Different schema settings are available per component // E.g. TextFields have minLength and maxLength. "schema": { // ... }}Lists
Lists are used to populated selection components like Radios and Selects
{ // Each list is identified by an UUID "id": "23d5309e-1aed-427d-b8ee-87e14f673e7f",
// A list name, title, type and items are all required "name": "colours", // Unused (deprecated) "title": "Colours", "type": "string", // Can also be "number" "items": [ { // Each list item is identified by an UUID "id": "bedd5984-fa95-48f9-87e2-1089d66574b2",
// List item text and value are both required. // If the list type is "number", the value should be numeric "text": "Red", "value": "red" }, { "id": "45c4bd8d-936f-4dda-b6a8-64c9d2532f10", "text": "Blue", "value": "blue" }, // ... ]}Conditions
Conditions bring logic to the form, when assigned to a page they make the page "conditional" and the page is only visited if the condition evaluates to "truthy"
{ // Each condition is identified by an UUID "id": "0e7ae320-c876-40c2-8803-7848cc49689b",
// Condition displayName should be unique "displayName": "faveColourIsRed",
"items": [ { // Each condition item is identified by an UUID "id": "f03a6735-0f7c-4dc9-b65c-7c42fcd0d189",
// `componentId` is a reference to the component "componentId": "fa67e20d-a89b-4e8a-85ec-8a63923b7137",
// Condition `operator` is a comparison operator ('is', 'is not', 'is longer than', 'contains', 'has length' etc.) "operator": "is",
// Conditions item values come in a few different forms:
// 1. `ListItemRef` - use these when the condition references a question (componentId) that is a list selection // The `value` of a `ListitemRef` should be an object with a listId and itemId keys pointing to the list and list item "type": "ListItemRef", "value": { "listId": "23d5309e-1aed-427d-b8ee-87e14f673e7f", // References the "Colours" list "itemId": "bedd5984-fa95-48f9-87e2-1089d66574b2" // References the "Red" item in the "Colours" list },
// 2. `RelativeDate` - relative date for date-based conditions // The `value` of a `RelativeDate` should be an object with a listId and itemId keys pointing to the list and list item "type": "RelativeDate", "value": { "period": 1, // Numeric amount of the time period "unit": "weeks", // Time unit (days, weeks, months, years), "direction": "future" // Temporal direction (either "past" or "future"') },
// 3. Scalar values can be `StringValue`, `NumberValue`, `BooleanValue` or `DateValue` // and are used to check absolute values of strings (TextField), numbers (NumberField), booleans (YesNoField) or dates (DatePartsField) // They are also used when the `operator` implies a numeric parameter e.g. 'has length' see below for examples. // The `value` of a scalar value condition should be a literal of the same type e.g. "type": "StringValue", "value": "Enrique Chase" } ],
// When the condition has 2 or more items, a coordinator is also required "coordinator": "and", // Supports both "and" and "or"}Condition examples
{ "name": "Example form asking what a users favourite animal are, with an condition based on their answer", "pages": [ { "id": "a86ea4ba-ae3b-4324-9acd-3a3f347cb0ec", "title": "What are your favourite animals", "path": "/favourite-animal", "components": [ { // ComponentId "id": "f0f67bf7-cdbb-4247-9f3c-8cd919183968", "type": "CheckboxesField", "title": "What are your favourite animals", "name": "nUaCCW", "shortDescription": "Favourite animals", "hint": "", "options": { "required": true }, "schema": {},
// References the "Animals" list "list": "0e047f83-dbb6-4c82-b709-f9dbaddf8644" } ], "next": [] } ], "conditions": [ { "items": [ { // This condition checks if the user chose "Monkey" as one of their favourite animals "id": "86e63584-12a8-4f2b-b51b-49765518b811", "componentId": "f0f67bf7-cdbb-4247-9f3c-8cd919183968", "operator": "contains", "type": "ListItemRef", "value": { // Reference to the "Animals" list "listId": "0e047f83-dbb6-4c82-b709-f9dbaddf8644", // Reference to "Monkey" in the "Animals" list "itemId": "0c546ae1-897e-48d0-9388-b0902fe23baf" } } ], "displayName": "FaveAnimalIsMonkey", "id": "8a3f6bb2-c305-410a-a037-7375be839105" } ], "sections": [], "lists": [ { "id": "0e047f83-dbb6-4c82-b709-f9dbaddf8644", "name": "sdewRT", "title": "Animals", "type": "string", "items": [ { "id": "fb3519b2-c6c7-40b6-8e03-2fb0db6d4f32", "text": "Horse", "value": "horse" }, { "id": "0c546ae1-897e-48d0-9388-b0902fe23baf", "text": "Monkey", "value": "monkey" }, { "id": "39f6fa65-1781-4569-9ba3-d8d13931f036", "text": "Giraffe", "value": "giraffe" } ] } ], "engine": "V2", "schema": 2}