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();
}
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)
);
}
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 })))
);
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])
);
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] || ''
};
}
XSS-safe display
All record data is HTML-escaped before insertion:
function escapeHtmlDisplay(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
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!
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)