DEV Community

Dev Nestio
Dev Nestio

Posted on

I Built a Browser-Based DNS Lookup Tool — A, AAAA, MX, TXT, SOA, CAA, SRV via DNS-over-HTTPS, 149 Tests

I Built a Browser-Based DNS Lookup Tool — A, AAAA, MX, TXT, NS, SOA, CAA, SRV via DNS-over-HTTPS, 149 Tests

Looking up DNS records usually means opening a terminal and typing dig or nslookup. But those tools aren't always available, and their output isn't always easy to read. So I built a browser-based DNS lookup tool that queries any record type via DNS-over-HTTPS and displays results in a clean, readable table.

👉 Try it live → dns-lookup-tool-bd4.pages.dev

No server-side code. No installation. Works in any browser.


What it does

Record types supported

  • A — IPv4 address
  • AAAA — IPv6 address
  • MX — Mail exchange (with priority)
  • TXT — Text records (SPF, DKIM, verification tokens)
  • NS — Nameserver records
  • CNAME — Canonical name
  • SOA — Start of authority (serial, refresh, retry, expire, TTL)
  • PTR — Reverse DNS
  • CAA — Certification authority authorization
  • SRV — Service records (priority, weight, port, target)
  • ANY — All records (via DoH)
  • ALL — Parallel queries for A, AAAA, MX, TXT, NS, CNAME, SOA simultaneously

DNS-over-HTTPS providers
Three providers selectable at runtime:

  • Cloudflare (cloudflare-dns.com/dns-query)
  • Google (dns.google/resolve)
  • Quad9 (dns.quad9.net/dns-query)

UX features

  • Quick-type chips: click A, MX, TXT, etc. to query immediately
  • ALL mode runs 7 query types in parallel and shows all results
  • Lookup history (last 12 queries) with one-click replay
  • Per-result Copy button outputs zone-file-style text
  • Timing shown in milliseconds

Special record formatting

  • MX: priority and mail server in separate columns
  • SOA: all 7 fields (mname, rname, serial, refresh, retry, expire, minimum) in labeled rows
  • SRV: priority, weight, port, target columns
  • CAA: flag, tag, value columns with quote-stripping

Technical details

DNS-over-HTTPS query

The browser has no native DNS API, but DNS-over-HTTPS resolves that. The fetch is straightforward:

async function queryDNS(domain, type) {
  const url = DOH_PROVIDERS[currentProvider];
  const params = new URLSearchParams({ name: domain, type });
  const res = await fetch(`${url}?${params}`, {
    headers: { 'Accept': 'application/dns-json' },
    signal: AbortSignal.timeout(8000)
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

The Accept: application/dns-json header is what requests the JSON format instead of binary DNS wire format.

Domain validation

function isValidDomain(domain) {
  if (!domain || domain.length > 253) return false;
  const d = domain.replace(/\.$/, ''); // allow trailing dot
  if (d.length === 0) return false;
  const labels = d.split('.');
  return labels.every(label =>
    label.length > 0 &&
    label.length <= 63 &&
    /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)
  );
}
Enter fullscreen mode Exit fullscreen mode

Enforces RFC 1035 label rules: no leading/trailing hyphens, max 63 chars per label, max 253 total, no underscores (though _ is valid in SRV record names — a known simplification).

ALL mode: parallel queries

When the user clicks ALL, 7 queries fire simultaneously:

const results = await Promise.allSettled(
  ALL_TYPES.map(t => queryDNS(domain, t).then(r => ({ type: t, data: r })))
);
Enter fullscreen mode Exit fullscreen mode

Promise.allSettled (not Promise.all) ensures a failed query for one type doesn't cancel the others. Only types that return records get a card.

Record type number map

DNS responses use numeric type codes, not strings:

const TYPE_MAP = {
  A: 1, NS: 2, CNAME: 5, SOA: 6, PTR: 12,
  MX: 15, TXT: 16, AAAA: 28, SRV: 33, CAA: 257, ANY: 255
};
const TYPE_NAME_MAP = Object.fromEntries(
  Object.entries(TYPE_MAP).map(([k,v]) => [v,k])
);
Enter fullscreen mode Exit fullscreen mode

This lets us filter the Answer array to only records matching the queried type, since ANY queries can return mixed types.

SOA record formatting

SOA data arrives as a space-delimited string — 7 fields in order:

case 'SOA': {
  const parts = String(data).split(' ');
  return {
    mname:   parts[0] || '',  // primary nameserver
    rname:   parts[1] || '',  // responsible mailbox (@ → .)
    serial:  parts[2] || '',
    refresh: parts[3] || '',
    retry:   parts[4] || '',
    expire:  parts[5] || '',
    minimum: parts[6] || ''
  };
}
Enter fullscreen mode Exit fullscreen mode

XSS-safe display

All record data is HTML-escaped before insertion:

function escapeHtmlDisplay(s) {
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
Enter fullscreen mode Exit fullscreen mode

TXT records can contain arbitrary content; this prevents any injected markup from executing.


Testing (149 tests, Node.js assert only)

No test framework. Just a small runner over const assert = require('assert'):

Category Tests
TYPE_MAP 11
TYPE_NAME_MAP 10
ALL_TYPES 8
isValidDomain 25
isValidIPv4 9
isValidIPv6 7
formatRecord 18
escapeHtmlDisplay 8
buildDoHUrl 10
History management 11
DOH_PROVIDERS 10
Integration 22
Total 149

Domain validation alone gets 25 tests because edge cases compound: trailing dots, empty labels, label length limits, hyphen rules, unicode, punycode.

node test/test.js

Total: 149  ✓ 149  ✗ 0
All 149 tests passed!
Enter fullscreen mode Exit fullscreen mode

Why DNS-over-HTTPS?

Browsers can't make raw UDP/TCP connections to port 53. DoH wraps DNS in standard HTTPS, making it accessible via fetch. All three supported providers (Cloudflare, Google, Quad9) implement the same JSON wire format, so the client code is provider-agnostic.


Try it

👉 dns-lookup-tool-bd4.pages.dev

Also accessible through the devnestio hub: devnestio.pages.dev/dns-lookup-tool/


Part of the devnestio collection — free, browser-only developer tools.

Top comments (0)