Build a domain availability form
Check an exact domain name and show available, taken, or unknown without treating a failed lookup as availability.
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();Check the name someone wants to use
Add a domain field to a project or website setup flow. Send the exact name to ParseAPI from your server, then show its registration state beside the field.
The result has three states: available, taken, and unknown. Registration availability does not establish purchase eligibility, a price, or a reservation. A person still needs to confirm those details when purchasing.
1. Collect the exact domain name
Give the form a single named input:
const form = `<form method="post" action="/">
<label for="domain">Domain name</label>
<input id="domain" name="domain" type="text" maxlength="253" required
autocomplete="off" autocapitalize="none" spellcheck="false"
placeholder="example.com" value="${escape(fields.domain ?? '')}"
aria-describedby="domain-help">
<p id="domain-help" class="note">Enter an exact domain, without https:// or a path.</p>
<button type="submit">Check domain</button>
</form>`;Accept a hostname such as example.com, rather than a URL or email address. The server trims it, normalizes internationalized names to ASCII, and removes a trailing dot. It preserves the actual name: shop.example.com is not silently rewritten to example.com.
Keep the SDK and secret PARSEAPI_KEY on the server. The local app uses Node's HTTP server, so the browser only submits the form to the app.
2. Ask Domain about registration
Call the official SDK with the normalized name:
const domain = await parse.domain(name, { signal });The response returns domain and available. Check that domain matches the requested name before rendering the result.
This is a registration lookup. DNS records, an HTTP response, or an absent website do not establish that a domain can be registered. No DNS shortcut or extra lookup is needed here.
3. Keep all three availability states
Branch on the boolean or null explicitly:
const state = domain.available === true ? 'available'
: domain.available === false ? 'taken' : 'unknown';true reports an unregistered name, false reports a registered name, and null means availability could not be established. A registry name outside supported coverage can remain unknown. Do not use !domain.available, which would merge unknown with a known negative.
A failed HTTP request or malformed response also leaves availability unknown. The app makes one attempt per submission and offers a retry rather than silently repeating it.
4. Return the result to the setup flow
Render the exact checked name with a useful next step:
const title = { available: 'Available', taken: 'Taken', unknown: 'Availability unknown' }[result.state];
const explanation = {
available: 'The registration lookup reports this exact name as unregistered. '
+ 'Confirm eligibility, price, and availability with your registrar before buying.',
taken: 'This exact name is registered. Try another name.',
unknown: 'The registration lookup could not establish availability. Try again later.'
}[result.state];
return `<section class="result"><h2>${title}</h2>
<p>${escape(result.name)}</p><p>${explanation}</p></section>`;In your application, keep the chosen name in the project setup draft and recheck before any existing purchase flow. This example neither purchases nor reserves a domain. Editing the name immediately clears the old result and cancels an in-flight request, so a late response cannot mark the new name available.
The Domain reference explains registration behavior and additional registration details.
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, domainToASCII, domainToUnicode } 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]));
function exactDomain(value) {
if (typeof value !== 'string' || value.trim().length > 253 || /[\s\u0000-\u0020\u007f/:@?#\\%\[\]]/u.test(value.trim())) throw new InputError('Enter one domain without a URL, path, or email address.');
const domain = domainToASCII(value.trim().toLowerCase()).replace(/\.$/, '');
const pattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9](?:[a-z0-9-]{0,57}[a-z0-9])?)$/;
if (!domain || domain.length > 253 || !pattern.test(domain) || /(?:^|\.)xn--/i.test(domainToUnicode(domain))) throw new InputError('Enter a valid domain name.');
return domain;
}
const TITLE = 'Check a domain name';
const INTRO = 'Check the registration state of the exact name you have in mind.';
const FAILURE = 'Registration could not be checked. Availability is unknown. Try again.';
const FIELDS = ['domain'];
export async function checkDomain(input, parse, signal) {
const name = exactDomain(input);
// example:request
const domain = await parse.domain(name, { signal });
// /example:request
if (!domain || domain.domain !== name || !(domain.available === null || typeof domain.available === 'boolean')) throw new Error('Unexpected domain response.');
// example:result
const state = domain.available === true ? 'available'
: domain.available === false ? 'taken' : 'unknown';
// /example:result
return { name: domain.domain, state };
}
function renderForm(fields = {}) {
// example:form
const form = `<form method="post" action="/">
<label for="domain">Domain name</label>
<input id="domain" name="domain" type="text" maxlength="253" required
autocomplete="off" autocapitalize="none" spellcheck="false"
placeholder="example.com" value="${escape(fields.domain ?? '')}"
aria-describedby="domain-help">
<p id="domain-help" class="note">Enter an exact domain, without https:// or a path.</p>
<button type="submit">Check domain</button>
</form>`;
// /example:form
return form;
}
async function submit(fields, parse, signal) {
const result = await checkDomain(fields.domain, parse, signal);
// example:integration
const title = { available: 'Available', taken: 'Taken', unknown: 'Availability unknown' }[result.state];
const explanation = {
available: 'The registration lookup reports this exact name as unregistered. '
+ 'Confirm eligibility, price, and availability with your registrar before buying.',
taken: 'This exact name is registered. Try another name.',
unknown: 'The registration lookup could not establish availability. Try again later.'
}[result.state];
return `<section class="result"><h2>${title}</h2>
<p>${escape(result.name)}</p><p>${explanation}</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 domain-search-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 Domain API reference.