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

ParseAPI
Node

Set up ParseAPI

Install the SDK in your application and set PARSEAPI_KEY in your server environment.

Terminal
npm install @parseapi/sdk@1.14.0
export PARSEAPI_KEY='YOUR_SECRET_API_KEY'

Create a client and reuse it for the calls below:

JavaScript
import { parseAPI } from '@parseapi/sdk';

const parse = parseAPI();
Get an API key ↗

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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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
domain-search-form.node.mjs
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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:

Terminal
node domain-search-form.node.mjs

Open 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.