JavaScript Your server
// contact.js: the route between the front end and the Core dna form
// (Express 4; app.use(session(...)) from express-session must run before it)
import express from 'express';
const SITE = process.env.COREDNA_URL; // https://www.example.com
const FORM_ID = process.env.COREDNA_CONTACT_FORM_ID; // the form's id
const FIELDS = ['name', 'email', 'message']; // its element names
export const contact = express.Router();
async function send(method, path, answers) {
const response = await fetch(SITE + '/cdna-api/forms/' + path, {
method,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, // no credentials
body: JSON.stringify(answers),
});
return { status: response.status, body: await response.json().catch(() => ({})) };
}
function errorsFrom(body) { // validationErrors are keyed by element name
const errors = {};
for (const [name, list] of Object.entries(body.validationErrors ?? {})) {
errors[name] = [].concat(list).map((e) => e.message ?? e).join(' ');
}
if (Object.keys(errors).length === 0) errors._form = body.message ?? 'The form could not be sent.';
return errors;
}
contact.post('/api/contact', express.json({ limit: '100kb' }), async (req, res) => {
const answers = Object.fromEntries(FIELDS.filter((f) => f in req.body).map((f) => [f, req.body[f]]));
const token = req.session.contactToken;
let r = token
? await send('PUT', 'submission/' + encodeURIComponent(token), answers)
: await send('POST', 'submission/' + FORM_ID, answers);
if (r.status === 409) { // completed, or the form changed: start again
delete req.session.contactToken;
r = await send('POST', 'submission/' + FORM_ID, answers);
}
switch (r.status) {
case 200:
delete req.session.contactToken;
return res.json({ status: 'done' });
case 207: // stored, not complete: continue it next time
req.session.contactToken = r.body.data?.formSubmission?.token;
return res.status(422).json({ status: 'invalid', errors: errorsFrom(r.body) });
case 422:
return res.status(422).json({ status: 'invalid', errors: errorsFrom(r.body) });
case 202:
delete req.session.contactToken;
if (r.body.redirectUrl) return res.json({ status: 'pay', redirectUrl: r.body.redirectUrl });
break;
}
console.error('Core dna form', r.status, r.body.message);
res.status(502).json({ status: 'failed', errors: { _form: 'The form could not be sent. Please try again later.' } });
});
JavaScript React
// ContactForm.jsx
import { useState } from 'react';
export function ContactForm() {
const [errors, setErrors] = useState({});
const [state, setState] = useState('editing'); // editing, sending, done
async function onSubmit(event) {
event.preventDefault();
setState('sending');
const answers = Object.fromEntries(new FormData(event.currentTarget));
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(answers),
});
const result = await response.json().catch(() => ({ errors: { _form: 'The form could not be sent.' } }));
if (result.status === 'done') return setState('done');
if (result.status === 'pay') return window.location.assign(result.redirectUrl);
setErrors(result.errors ?? {});
setState('editing');
}
if (state === 'done') return <p role="status">Thank you. We reply within two working days.</p>;
return (
<form onSubmit={onSubmit} noValidate>
<label>Name <input name="name" /></label>
{errors.name && <p className="field-error">{errors.name}</p>}
<label>Email <input name="email" type="email" /></label>
{errors.email && <p className="field-error">{errors.email}</p>}
<label>Message <textarea name="message" /></label>
{errors.message && <p className="field-error">{errors.message}</p>}
{errors._form && <p className="form-error" role="alert">{errors._form}</p>}
<button disabled={state === 'sending'}>Send</button>
</form>
);
}
HTML Vue
<!-- ContactForm.vue -->
<script setup>
import { ref } from 'vue';
const errors = ref({});
const state = ref('editing');
async function onSubmit(event) {
state.value = 'sending';
const answers = Object.fromEntries(new FormData(event.target));
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(answers),
});
const result = await response.json().catch(() => ({ errors: { _form: 'The form could not be sent.' } }));
if (result.status === 'done') { state.value = 'done'; return; }
if (result.status === 'pay') { window.location.assign(result.redirectUrl); return; }
errors.value = result.errors ?? {};
state.value = 'editing';
}
</script>
<template>
<p v-if="state === 'done'" role="status">Thank you. We reply within two working days.</p>
<form v-else @submit.prevent="onSubmit" novalidate>
<label>Name <input name="name"></label>
<p v-if="errors.name" class="field-error">{{ errors.name }}</p>
<label>Email <input name="email" type="email"></label>
<p v-if="errors.email" class="field-error">{{ errors.email }}</p>
<label>Message <textarea name="message"></textarea></label>
<p v-if="errors.message" class="field-error">{{ errors.message }}</p>
<p v-if="errors._form" class="form-error" role="alert">{{ errors._form }}</p>
<button :disabled="state === 'sending'">Send</button>
</form>
</template>
The route's behaviour follows the platform's code for these calls; this documentation site has no test form to capture real answers from. The statuses and what they mean are set out, with the body for each element type, in Submitting a form through the API.