Introduction
Still & Spoke's labels, conditional seating note and responsive layout are ready. The current work handler can show a demonstration confirmation without sending a booking. You will replace it with a real asynchronous request and feedback that reflects what the studio actually accepted. A correct-looking page is not a booking until the prepared service returns its confirmation.
You need HTML forms, basic CSS layout, JavaScript objects, functions, DOM selection and events. This lab teaches JSON, async/await, fetch, HTTP outcomes, pending state and recoverable feedback. The form semantics and conditional constraints are prepared here; completing the previous lab is useful but its VM files are not required. Only edit /home/labex/project/stillspoke/request.js and feedback.js. Preserve other frontend files, the API, capacity and existing visitors. All identities are synthetic; no email or payment is sent. The 60-minute duration is an author estimate, not a measured beginner result.
Use Workspace for complete file replacements and Studio for the website. Study form is an independent working example; its bookings never complete the work page. Studio records reads accepted bookings. Practice room is a separate training page that can refuse one request before accepting anything. Every lab starts with its own independent data. This lab does not teach the prepared backend, production deployment, arbitrary request idempotency or full accessibility compliance.
Send One Real Request and Wait for Its Answer
In this step, replace the demonstration handler with an actual POST request, prevent duplicate activation while it is pending, and show a genuine server confirmation.
In Studio, choose Study form. Use Tab to Your name and enter Example Guest; Tab to Email and enter sample@example.test; Tab to Session, type C, and read Clay, slowly / 10:00. Tab past the unchecked assistance checkbox and optional note to Reserve my place. Press Enter once. Wait for the real Confirmation SS-…, then choose Studio records and match its name, email and time. Return using Back to signup and Work form. That example has a separate database. Your work page still has only the existing visitors; its current demonstration text cannot establish acceptance.
The study form shows the expected visible result: a separately headed confirmation with a readable time. Your number will differ.

The prepared API is part of this local environment. These are its public fields and rules:
| JSON field | Meaning |
|---|---|
name |
At least two characters after trimming outer whitespace. |
email |
A synthetic address ending in @example.test. Native email syntax is checked first in the browser. |
session |
A listed stable ID: clay-morning for 10:00 or clay-afternoon for 14:00. Afternoon is already full. |
support |
A Boolean indicating whether seating assistance is selected. |
note |
A string, at least five characters when assistance is selected; otherwise optional. |
A POST sends a proposed new booking to api/bookings. This is a relative URL under the current work or example page, so each stays in its own business space. The API returns JSON, a text format for objects, strings, numbers and Booleans. Object property names and JSON strings use double quotes. JSON.stringify(data) converts our JavaScript object to JSON text; response.json() parses response text back into an object. The header Content-Type: application/json tells the service what the request body contains. Neither the frontend's text nor a disabled button implements the service's capacity rule.
HTTP response statuses distinguish outcomes:
| Status | Response and what it establishes |
|---|---|
201 |
booking contains the accepted fields, readable sessionTitle and server-generated id. A booking was accepted; its confirmation number begins with SS-. Never invent one. |
422 |
errors maps field names to explanations. Nothing was booked; correct the marked fields. |
409 |
The selected session is full. Nothing was booked; choose an available session. |
503 |
The controlled practice pause refused the request before acceptance. Nothing was booked; retry is safe. |
fetch returns a Promise, representing a result that will arrive later. An async event handler can use await to pause its own work until the Promise settles while the browser remains usable. Await the HTTP response, then await JSON parsing. An HTTP refusal such as 422 or 503 still resolves fetch; it does not automatically enter catch. Check the status explicitly. A network or parsing failure can enter catch without establishing whether a booking was accepted. Its message must say acceptance is unknown, with the Studio records recovery path.
The form's submit event covers both a button activation and Enter in a field. event.preventDefault() prevents the browser's normal navigation submission so this handler can send JSON. Native invalid fields stop submission before this event reaches our handler. form.elements looks up controls by their name, not their visible position or HTML id. Text fields use .value; the checkbox uses .checked.
Set pending before awaiting any work. The guard returns early if another submit event arrives while pending, and .disabled gives a visible native disabled button. This prevents repeated activation in this page during one in-flight request. It is not a guarantee against other browser pages or arbitrary concurrent clients. The try block performs the request; catch gives an unknown-acceptance message on failure; finally restores the button and clears the passive sending message for every outcome. Keep the user's field values instead of resetting the form.
clearErrors, showMessage and showResponse are functions in feedback.js. The prepared page loads scenario.js, validity.js, feedback.js and request.js with defer, in that order after HTML parsing. The first supplies data, the second supplies the native conditional rule, and the last two cooperate on request and feedback. The current feedback file handles a normal 201; you will improve field refusals next.
In Workspace, use cd to choose the workspace. cat > request.js replaces that one file. The quoted here-document marker JS makes the file content literal until the final line containing only JS; paste the entire block without Markdown backticks. When the shell prompt returns, the file has been saved.
cd /home/labex/project/stillspoke
cat > request.js <<'JS'
const form = document.querySelector('form');
const reserveButton = form.querySelector('[type="submit"]');
const progress = document.querySelector('#progress');
let pending = false;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (pending) return;
clearErrors();
document.querySelector('#result').hidden = true;
const data = {
name: form.elements.name.value,
email: form.elements.email.value,
session: form.elements.session.value,
support: form.elements.support.checked,
note: form.elements.note.value
};
pending = true;
reserveButton.disabled = true;
progress.textContent = 'Sending your request...';
try {
const response = await fetch('api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const body = await response.json();
showResponse(response.status, body);
} catch (error) {
showMessage('No confirmation received. Acceptance is unknown. Check Studio records; wait for the original attempt to be resolved before a new submission.');
} finally {
pending = false;
reserveButton.disabled = false;
progress.textContent = '';
}
});
JS
Return to Studio, refresh the work page, then enter Morgan Vale, morgan@example.test, choose Clay, slowly / 10:00, and leave assistance unchecked. Press Tab to the submit button and Enter once. While Sending your request... is visible the button is disabled. Wait for Submission result to show Reserved for Morgan Vale, a real Confirmation SS-…, and the readable 10:00 title. The passive role="status" message is never focused; the ordinary result section can receive focus to make its heading and result easy to find.
Choose Studio records, match the complete new booking and preserve Existing Visitor and Avery Reed. The old demonstration ID must not appear as an accepted booking. A successful HTTP transfer alone is insufficient: the response must actually be 201, and the confirmation must come from its accepted booking. Self-check: a real accepted record exists, one in-flight submission disables repeated activation, and the handler restores the button afterward.
Associate a Server Refusal with the Right Field
In this step, turn a real server validation refusal into a readable field error without erasing the visitor's other input.
A browser can reject malformed email syntax, but the prepared service also has a business rule: practice addresses must end in @example.test. The address morgan@other.test is syntactically valid, so it reaches the server and produces HTTP 422. This is a real request and refusal, distinct from the browser's required-field popup.
An error response has this shape; this is data to read, not a command:
{
"message": "Please correct the marked fields. Nothing was booked.",
"errors": {
"email": "Use an address ending in @example.test for this practice."
}
}
errors.email refers to the API key email. Match it through form.elements[name], not through the order of controls or the first error paragraph. A string key inside brackets selects the corresponding object property. Object.keys(body.errors) produces an array of its property names; for (const name of ...) visits each name. A variable declared with let can be assigned later, so first remembers the first marked field.
textContent displays error and result strings as text, rather than interpreting a server message as HTML. aria-invalid="true" marks a field invalid; the associated email-error paragraph supplies a visible reason through the form's existing aria-describedby. Do not replace the persistent help with a colour alone. Clearing earlier errors before a new request prevents an old message from describing a now-corrected field. On a 422, focus the first marked input after showing the overall result. On other outcomes, the ordinary result section can receive focus. A passive live status announcement is a separate element and should not be forcibly focused.
The success message uses a template literal: backticks surround the text, and ${booking.id} inserts the current property value. This constructs a string, which textContent displays literally. The confirmation comes from the response object, not from a locally invented number.
In Workspace, replace feedback.js with these functions:
cd /home/labex/project/stillspoke
cat > feedback.js <<'JS'
function clearErrors() {
for (const field of document.querySelectorAll('form [name]')) {
field.removeAttribute('aria-invalid');
const error = document.getElementById(field.name + '-error');
if (error) error.textContent = '';
}
}
function showMessage(text) {
const panel = document.querySelector('#result');
document.querySelector('#message').textContent = text;
panel.hidden = false;
panel.focus();
}
function showResponse(status, body) {
if (status === 201) {
const booking = body.booking;
showMessage(`Reserved for ${booking.name}. Confirmation ${booking.id}. ${booking.sessionTitle}.`);
} else if (status === 422) {
showMessage(body.message);
let first;
for (const name of Object.keys(body.errors)) {
const field = document.querySelector('form').elements[name];
document.getElementById(name + '-error').textContent = body.errors[name];
field.setAttribute('aria-invalid', 'true');
if (!first) first = field;
}
first.focus();
} else {
showMessage(body.message);
}
}
JS
Return to Studio, refresh the work page, and enter Morgan Vale, morgan@other.test, Clay, slowly / 10:00, assistance selected, and A seat near the entrance. Submit once. You should see the service's overall Nothing was booked message and a new email-specific error. Focus moves to Email, its visible label and help remain, and your name, session, checkbox and note remain unchanged. Studio records should not gain a booking for the refused address. If you want to inspect records without leaving this draft, open the Studio records link in another browser page using your browser's Open Link in New Tab command, then return to the original form page.
Correct only the address to morgan@example.test and submit again. The old field error is cleared, a genuine new confirmation appears, and the note reaches the accepted record. Also test a different field refusal: with the other inputs valid, replace the name with exactly two spaces. Native minlength="2" counts those typed characters, but the published server rule trims outer whitespace before requiring two characters. The real 422 must mark and focus Your name, not always Email, while retaining the email, session and note. Restore Morgan Vale afterward. Reusing this synthetic name in a second accepted booking is permitted; this exercise is not an email uniqueness system. Existing visitors remain.
If you accidentally type morgan.example.test without @, the browser will refuse it before a POST. That native message is expected but does not test the 422 branch. Replace it with the native-valid morgan@other.test to exercise the server rule. If feedback never updates after a code edit, use cat feedback.js in Workspace, compare it with the complete block and refresh Studio after correcting a typo. Do not erase data or restart the service to repair a JavaScript spelling mistake. Self-check: the actual server field key is associated with its matching input and readable error; other input survives; correction and resubmission work.
Recover from Full Sessions and a Safe Service Pause
In this step, distinguish a full session from a pre-acceptance service refusal, keep the draft, and complete a safe retry.
In Studio, refresh the work page and enter Casey Brook, casey@example.test, Clay, slowly / 14:00, assistance selected, and A seat near the entrance. Use the session selector to read the 14:00 title before submitting. Typing C initially selects the first matching Clay option; typing it again cycles to the next matching option. Read the visible choice rather than assuming a key always means the same position.
Submit once. The prepared service returns HTTP 409 because that session is full. Read the visible That session is full. Nothing was booked; choose another session. result. Your input remains and the submit button becomes available. Do not clear the existing 14:00 visitor to make space. The 409 branch does not require an invalid email or a malformed request; it is a business refusal of a correctly formed proposal.
Change only the session to Clay, slowly / 10:00. Choose the Practice room page link; it opens a separate browser page so your original form remains open. This is a training area, separate from the customer signup. Choose Pause next submission and wait for Next submission will be refused before a booking is accepted. Close that practice page or return to the original Studio form. Do not use the practice page's Back to signup link to replace your still-open draft.
The separate training page states the pre-acceptance promise. Keep the original form open while using this control.

Submit the original form once. This time the service returns HTTP 503 and explicitly says Nothing was booked. Service paused; please retry. Your name, address, selected 10:00 session, checkbox and note remain. The pause is consumed by this request. With the original input still present, press Enter on Reserve my place once more. Wait for the real 201 confirmation, then use Studio records to match Casey Brook, the email, 10:00 time and seating note. Existing visitors and earlier accepted bookings stay.
The 503 practice contract is narrow: it refuses before accepting a booking, which makes this retry safe. A lost connection or an unreadable response is different. catch says acceptance is unknown; it does not say Nothing was booked. In that situation, open Studio records in another browser page. A matching record may help you find an existing confirmation, but repeated names and values do not always identify which attempt created it. Do not submit again while the original attempt is unresolved. Even a successful records read with no match does not prove rejection: the original request may still be in flight. Wait for that attempt to be resolved or ask the studio operator (or your instructor in this practice environment). If records cannot be read, wait for the service as well. Only the explicitly pre-acceptance 503 refusal in this exercise establishes a safe retry. This lab does not promise idempotency or teach a distributed payment workflow.
For an ordinary editing recovery, switch to Workspace and display the saved request file:
cd /home/labex/project/stillspoke
cat request.js
Check the exact relative URL api/bookings, the spelling JSON.stringify, both await expressions and the finally block. If a typo changed them, replace the file using the full Step 1 block, refresh Studio. Before any new submission, resolve any earlier unknown attempt using the boundary above; editing a typo alone does not establish that a previous request was rejected. When that attempt is known resolved, repeat the relevant normal test with a new synthetic name. The read-only lab check does not install a replacement handler or submit a live booking to repair your work.
Self-check: full and paused outcomes give visible results after pending ends, preserve the draft, accept correction or safe retry, and never display a fabricated accepted number. Equivalent understandable error wording is fine. A normal accepted record still exists, and the entire prepared capacity and visitor state remains intact.
Summary
You sent JSON through an awaited POST, distinguished HTTP refusals from connection uncertainty, guarded pending activation and displayed the actual accepted confirmation. You associated server field errors with their controls, retained input through full and paused outcomes, and used records to guide recovery without assuming every failed connection means nothing was accepted.
