Recheck a booking date before accepting it
Build a date form that checks your business weekday, holiday, and closure policy again on the server.
Working example
ParseAPISet up ParseAPI
Install the SDK in your application and set PARSEAPI_KEY in your server environment.
npm install @parseapi/sdk@1.14.0
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'Create a client and reuse it for the calls below:
import { parseAPI } from '@parseapi/sdk';
const parse = parseAPI();Accept dates that fit your business calendar
A date picker can collect a valid date without knowing whether your business is open. Build a booking date form that checks the submitted date on the server and explains why it is accepted or needs changing.
This example offers weekday appointments in America/New_York, closes on US federal public holidays and December 24, 2026, and accepts dates from tomorrow through the next 180 days. These are explicit business rules. Passing them does not reserve a slot or establish staff capacity.
1. Collect one appointment date
Use a named date input so the submitted value reaches the server as date:
const form = `<form method="post" action="/">
<label for="date">Appointment date</label>
<input id="date" name="date" type="date" required
min="${isoDay(today + 1)}" max="${isoDay(today + 180)}"
value="${escape(fields.date ?? isoDay(today + 1))}"
aria-describedby="policy">
<p id="policy" class="note">Monday-Friday. US federal holidays and
December 24, 2026 are closed. Business time zone: America/New_York.</p>
<button type="submit">Submit date</button>
</form>`;The input's bounds guide the person choosing a date. The server repeats those bounds using the business time zone, then rejects weekends and the business's own closed dates. A caller can edit browser constraints, so the submit handler owns the policy.
The complete app uses Node's HTTP server and a secret PARSEAPI_KEY on the server. Its SDK client disables automatic retries so one submission makes one holiday attempt.
2. Recheck the submitted date with Holiday
For a candidate weekday, call the SDK's date operation with the exact submitted date:
const calendar = await parse.holiday.date('US', date, { signal });The response identifies country and date, with holiday containing a published holiday or null. The complete source checks that context before using the answer. A missing field or failed request leaves the date unaccepted.
This example uses the US federal calendar. Choose the holiday scope your business actually follows before adapting it to another country or a regional calendar.
3. Turn the holiday into an acceptance decision
A public holiday closes this business. An observance does not, and holiday: null means the calendar has no holiday for that date:
const holiday = calendar.holiday;
if (holiday?.type === 'public') {
return { accepted: false, date, reason: `Closed for ${holiday.name}.` };
}
return {
accepted: true, date,
reason: 'This date meets the business weekday, holiday, and closure policy.'
};Use the returned holiday.name to explain the rejection. A substitute holiday already carries its published date; leave it on that date rather than shifting it to another Monday.
An unavailable calendar is different from holiday: null. Keep the form available to retry and show that no date was accepted when the request fails.
4. Use the server's decision in the booking flow
The POST handler renders the checked date and its reason:
return `<section class="result">
<h2>${decision.accepted ? 'Date accepted by policy' : 'Choose another date'}</h2>
<p><time datetime="${escape(decision.date)}">${escape(decision.date)}</time></p>
<p>${escape(decision.reason)}</p>
<p class="note">This checks the date policy. It does not reserve a time slot.</p></section>`;In your application, put the same decision immediately before the existing booking write. Continue only when accepted is true, and keep your existing slot, capacity, and conflict checks there too. Do not accept an earlier browser result as the final decision.
The standalone app stops at date-policy acceptance and stores no bookings. Editing the input clears an earlier result and cancels an in-flight check. Submitted values and holiday names are escaped before rendering.
See the Holiday reference for calendar coverage and date behavior. For a preparation estimate with a cutoff, build a checkout dispatch date.
Complete example
Use the complete implementation as a reference when adding this feature to your app. It includes the validation and failure handling around the calls above.
View and copy the complete source
import { createServer } from 'node:http';
import { pathToFileURL } from 'node:url';
// example:imports
import { parseAPI } from '@parseapi/sdk';
// /example:imports
class InputError extends Error {}
const escape = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
const text = (value, max = 160) => typeof value === 'string' && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/u.test(value);
const DAY = 86400000;
function civilDay(value) {
const time = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) ? Date.parse(`${value}T00:00:00Z`) : NaN;
if (!Number.isFinite(time) || value.startsWith('0000') || new Date(time).toISOString().slice(0, 10) !== value) throw new InputError('Choose a real date.');
return time / DAY;
}
const isoDay = day => new Date(day * DAY).toISOString().slice(0, 10);
function businessClock(now) {
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) throw new Error('Clock unavailable.');
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
}).formatToParts(now).map(part => [part.type, part.value]));
return { date: `${parts.year}-${parts.month}-${parts.day}`, minutes: Number(parts.hour) * 60 + Number(parts.minute) };
}
function readHoliday(holiday, date) {
if (!holiday || holiday.date !== date || !text(holiday.name) || !['public', 'observance'].includes(holiday.type) ||
typeof holiday.substitute !== 'boolean' || !(holiday.regions === null || Array.isArray(holiday.regions) && holiday.regions.length <= 100 && holiday.regions.every(region => text(region, 20)))) throw new Error('Unexpected holiday.');
civilDay(holiday.date);
// This example's policy is US federal only, without state-specific holidays.
if (holiday.type === 'public' && holiday.regions !== null) throw new Error('Calendar scope does not match the policy.');
return holiday;
}
export const POLICY = { country: 'US', zone: 'America/New_York', closedDates: ['2026-12-24'], maxDays: 180 };
const TITLE = 'Choose an appointment date';
const INTRO = 'Weekday appointments, excluding US federal holidays and December 24, 2026. Dates are checked again when you submit.';
const FAILURE = 'The holiday calendar could not be checked. Your date has not been accepted. Try again.';
const FIELDS = ['date'];
export async function checkBookingDate(date, parse, signal, now = new Date()) {
const day = civilDay(date), today = civilDay(businessClock(now).date);
if (day <= today || day > today + POLICY.maxDays) throw new InputError('Choose a date from tomorrow through the next 180 days in New York.');
if ([0, 6].includes(new Date(day * DAY).getUTCDay())) return { accepted: false, date, reason: 'Appointments run Monday through Friday.' };
if (POLICY.closedDates.includes(date)) return { accepted: false, date, reason: 'The business is closed on this date.' };
// example:request
const calendar = await parse.holiday.date('US', date, { signal });
// /example:request
if (!calendar || calendar.country !== 'US' || calendar.date !== date || !Object.hasOwn(calendar, 'holiday')) throw new Error('Unexpected calendar response.');
if (calendar.holiday !== null) readHoliday(calendar.holiday, date);
// example:result
const holiday = calendar.holiday;
if (holiday?.type === 'public') {
return { accepted: false, date, reason: `Closed for ${holiday.name}.` };
}
return {
accepted: true, date,
reason: 'This date meets the business weekday, holiday, and closure policy.'
};
// /example:result
}
function renderForm(fields = {}, now = new Date()) {
const today = civilDay(businessClock(now).date);
// example:form
const form = `<form method="post" action="/">
<label for="date">Appointment date</label>
<input id="date" name="date" type="date" required
min="${isoDay(today + 1)}" max="${isoDay(today + 180)}"
value="${escape(fields.date ?? isoDay(today + 1))}"
aria-describedby="policy">
<p id="policy" class="note">Monday-Friday. US federal holidays and
December 24, 2026 are closed. Business time zone: America/New_York.</p>
<button type="submit">Submit date</button>
</form>`;
// /example:form
return form;
}
async function submit(fields, parse, signal, now) {
const decision = await checkBookingDate(fields.date, parse, signal, now);
// example:integration
return `<section class="result">
<h2>${decision.accepted ? 'Date accepted by policy' : 'Choose another date'}</h2>
<p><time datetime="${escape(decision.date)}">${escape(decision.date)}</time></p>
<p>${escape(decision.reason)}</p>
<p class="note">This checks the date policy. It does not reserve a time slot.</p></section>`;
// /example:integration
}
const CSS = `
:root{color-scheme:light dark;font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#202124;background:#f5f5f7}
*{box-sizing:border-box}body{margin:0}main{max-width:720px;margin:0 auto;padding:56px 20px 72px}h1{font-size:clamp(2rem,6vw,2.8rem);line-height:1.12;letter-spacing:-.045em;margin:0 0 16px}h2{font-size:1.35rem;line-height:1.3;letter-spacing:-.025em;margin:0 0 12px}h3{font-size:1rem;margin:0 0 8px}p{margin:0 0 16px}.intro,.note,small{color:#62646a}.intro{max-width:56ch}form,.result,.account{background:#fff;border:1px solid #e2e3e4;border-radius:18px;padding:24px;margin-top:24px}label{display:block;font-weight:600;margin-bottom:8px}input,select,button{font:inherit;min-height:44px;border-radius:8px}input,select{width:100%;border:1px solid #acafb5;background:#fff;color:inherit;padding:10px 12px}select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' d='m4 6 4 4 4-4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:36px}button{border:0;background:#087f9c;color:#fff;font-weight:600;padding:10px 18px;margin-top:20px;cursor:pointer}button:disabled{opacity:.65;cursor:wait}:focus-visible{outline:3px solid #0891b2;outline-offset:3px}.note{font-size:.875rem;margin-top:12px}.status{min-height:1.5em;margin-top:14px}ul{padding-left:20px}li{overflow-wrap:anywhere}.result{overflow-wrap:anywhere}.result p:last-child,.account p:last-child{margin-bottom:0}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}.technology{display:block}time{font-variant-numeric:tabular-nums}
@media(max-width:480px){main{padding-top:32px}form,.result,.account{padding:20px}.grid{grid-template-columns:1fr}button{width:100%}}
@media(prefers-color-scheme:dark){:root{background:#000;color:#f5f5f7}form,.result,.account{background:#1d1d1f;border-color:#424245}input,select{background-color:#1d1d1f;border-color:#69696e}.intro,.note,small{color:#a1a1a6}button{background:#087f9c}}
`;
const CLIENT = `
const form = document.querySelector('form');
const result = document.querySelector('#result');
const status = document.querySelector('#status');
const button = form.querySelector('button');
let controller, revision = 0;
form.addEventListener('input', () => {
revision++; controller?.abort(); result.replaceChildren(); status.textContent = ''; button.disabled = false;
});
form.addEventListener('submit', async event => {
event.preventDefault(); controller?.abort(); controller = new AbortController();
const current = ++revision, signal = controller.signal;
result.replaceChildren(); status.textContent = 'Checking...'; button.disabled = true;
try {
const response = await fetch('/', { method: 'POST', signal, headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'X-Example-Fragment': '1'
}, body: new URLSearchParams(new FormData(form)) });
const html = await response.text();
if (current !== revision || signal.aborted) return;
if (!response.headers.get('content-type')?.startsWith('text/html')) throw new Error();
result.innerHTML = html; status.textContent = '';
} catch {
if (current !== revision || signal.aborted) return;
status.textContent = 'The check did not finish. Try again.';
} finally { if (current === revision) button.disabled = false; }
});
`;
function page(form, result = '') {
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${TITLE}</title><style>${CSS}</style></head><body><main><h1>${TITLE}</h1><p class="intro">${INTRO}</p>${form}<p id="status" class="status" role="status"></p><div id="result" aria-live="polite">${result}</div></main><script>${CLIENT}</script></body></html>`;
}
function readForm(req, allowed) {
if (req.headers['content-type']?.split(';')[0] !== 'application/x-www-form-urlencoded') return Promise.reject(new InputError('Submit this form again.'));
return new Promise((resolve, reject) => {
const chunks = []; let bytes = 0;
const fail = error => { cleanup(); req.resume(); reject(error); };
const chunk = value => {
bytes += value.length;
if (bytes > 4096) return fail(new InputError('The form is too large.'));
chunks.push(value);
};
const finish = () => {
cleanup(); const fields = new URLSearchParams(Buffer.concat(chunks).toString('utf8'));
if ([...fields.keys()].some(key => !allowed.includes(key)) || allowed.some(key => fields.getAll(key).length !== 1)) return reject(new InputError('Submit one value for each field.'));
resolve(Object.fromEntries(fields));
};
const aborted = () => fail(new Error('Request ended.'));
const cleanup = () => { req.off('data', chunk); req.off('end', finish); req.off('aborted', aborted); req.off('error', fail); };
req.on('data', chunk); req.once('end', finish); req.once('aborted', aborted); req.once('error', fail);
});
}
export function createApp(parse, { now = () => new Date() } = {}) {
const server = createServer(async (req, res) => {
const controller = new AbortController();
const disconnect = () => { if (!res.writableEnded) controller.abort(); };
res.once('close', disconnect);
const send = (status, html) => {
if (controller.signal.aborted) return;
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer',
'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'" });
res.end(html);
};
let fields = {};
const fragment = req.headers['x-example-fragment'] === '1';
try {
const host = req.headers.host ?? '';
if (!/^(?:127\.0\.0\.1|localhost)(?::[0-9]{1,5})?$/.test(host) ||
req.headers.origin && req.headers.origin !== `http://${host}` || req.headers['sec-fetch-site'] === 'cross-site') return send(403, '<p>Open the app on 127.0.0.1 to use this form.</p>');
if (req.url !== '/') return send(404, '<p>Page not found.</p>');
if (req.method === 'GET') return send(200, page(renderForm({}, now())));
if (req.method !== 'POST') return send(405, '<p>Use the form to submit a check.</p>');
fields = await readForm(req, FIELDS);
const result = await submit(fields, parse, controller.signal, now());
send(200, fragment ? result : page(renderForm(fields, now()), result));
} catch (error) {
if (controller.signal.aborted) return;
const message = error instanceof InputError ? error.message : FAILURE;
const result = `<section class="result" role="alert"><h2>Check unavailable</h2><p>${escape(message)}</p></section>`;
send(error instanceof InputError ? 400 : 502, fragment ? result : page(renderForm(fields, now()), result));
}
});
server.requestTimeout = 10000; server.headersTimeout = 10000;
return server;
}
function main() {
const key = process.env.PARSEAPI_KEY?.trim();
if (!key || key.startsWith('parse_public_') || key.startsWith('parse_app_') || /[\r\n]/.test(key)) throw new Error('Set PARSEAPI_KEY to a server secret key.');
const port = process.env.PORT === undefined ? 8000 : Number(process.env.PORT);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PORT must be from 1 to 65535.');
// API 2.1.0 is selected by @parseapi/sdk 1.14.0. No automatic retry of a submitted form.
// example:client
const parse = parseAPI(key, { retries: 0 });
// /example:client
const server = createApp(parse);
server.on('error', () => { console.error('The local app could not start. Check PORT.'); process.exitCode = 1; });
server.listen(port, '127.0.0.1', () => console.log(`Open http://127.0.0.1:${port}`));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; }
}
// The website embeds this same app; the standalone server remains unchanged.
export { page, renderForm, submit };
Try the complete example on your computer
After completing the setup above, save the source in your application folder and start the app:
node holiday-booking-form.node.mjsOpen 127.0.0.1:8000 to use the form. The server keeps your secret key out of the page.
For every field and parameter, see Holiday API reference.
Build something else
- Estimate a date in business days
- Find an open store nearby
- Show opening hours on a business page
- Show a dispatch estimate at checkout