Building a DNS Lookup Tool in the Browser: Google DNS API in Practice#

2026-04-29 18:25

While developing JsonKit, I needed to implement a DNS lookup tool. The traditional approach is to call dig or nslookup from the backend, but this time I wanted to try a pure frontend solution—directly calling the Google DNS API. Turns out there are quite a few gotchas. Here’s what I learned.

Why Google DNS API?#

DNS queries use UDP protocol, which browsers can’t directly send. So the frontend has to use an HTTP API. There are three main options:

  1. Google DNS API - https://dns.google/resolve
  2. Cloudflare DNS API - https://cloudflare-dns.com/dns-query
  3. Self-hosted backend proxy - Call system commands

Cloudflare’s API requires DOH (DNS over HTTPS) protocol and returns DNS wire format binary data, which is a pain to parse. Google’s API returns JSON directly, making it much more frontend-friendly:

// Google DNS API response format
{
  "Status": 0,
  "TC": false,
  "RD": true,
  "RA": true,
  "AD": false,
  "CD": false,
  "Question": [
    { "name": "google.com.", "type": 1 }
  ],
  "Answer": [
    { "name": "google.com.", "type": 1, "TTL": 299, "data": "142.250.189.238" }
  ]
}

Core Implementation: Types and Requests#

DNS has many record types. The common ones are A, AAAA, CNAME, MX, TXT, NS, SOA, SRV, and CAA. Let’s define the types:

const RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA'] as const
type RecordType = (typeof RECORD_TYPES)[number]

interface DnsRecord {
  name: string    // Domain name
  type: number    // DNS record type code (1=A, 28=AAAA)
  TTL: number     // Cache time (seconds)
  data: string    // Record value
}

The request logic is straightforward—just construct the URL:

const handleLookup = async () => {
  const trimmed = domain.trim()
  if (!trimmed) return

  // Domain validation
  const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
  if (!domainRegex.test(trimmed)) {
    setError('Invalid domain format')
    return
  }

  setLoading(true)
  const start = performance.now()

  try {
    const response = await fetch(
      `https://dns.google/resolve?name=${encodeURIComponent(trimmed)}&type=${recordType}`
    )
    const data = await response.json()
    const elapsed = Math.round(performance.now() - start)
    setQueryTime(elapsed)

    if (data.Answer && data.Answer.length > 0) {
      setResults(data.Answer)
    } else {
      setResults([])
    }
  } catch {
    setError('Query failed')
  } finally {
    setLoading(false)
  }
}

Gotcha #1: Domain Validation Edge Cases#

Domain validation looks simple, but there are many edge cases:

  • example.com
  • sub.example.com
  • a-b-c.example.com
  • -example.com ❌ (can’t start with hyphen)
  • example-.com ❌ (can’t end with hyphen)
  • example.com- ❌ (TLD can’t end with hyphen)
  • 123.example.com ✅ (can start with digit)

Here’s the regex I use:

const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/

Key points:

  • Each label is max 63 characters ({0,61} plus two boundary characters)
  • Can’t start or end with hyphen
  • TLD is at least 2 letters

Gotcha #2: DNS Record Type Codes#

Google API returns numeric type codes, not strings:

{ "name": "google.com.", "type": 1, "data": "142.250.189.238" }

Common type mappings:

const TYPE_MAP: Record<number, string> = {
  1: 'A',        // IPv4 address
  28: 'AAAA',    // IPv6 address
  5: 'CNAME',    // Alias
  15: 'MX',      // Mail server
  16: 'TXT',     // Text record
  2: 'NS',       // Name server
  6: 'SOA',      // Start of authority
  33: 'SRV',     // Service record
  257: 'CAA',    // Certificate authority authorization
}

Displaying raw numbers would confuse users. Convert at the UI layer:

const getTypeLabel = (type: number): string => {
  return TYPE_MAP[type] || `TYPE${type}`
}

Gotcha #3: MX Record Special Format#

MX record data field has format priority mailserver:

{ "name": "google.com.", "type": 15, "data": "10 smtp.google.com." }

Note the trailing dot (FQDN format). Format it for display:

const formatMxRecord = (data: string): { priority: number; server: string } => {
  const [priority, server] = data.split(' ')
  return {
    priority: parseInt(priority),
    server: server.replace(/\.$/, '') // Remove trailing dot
  }
}

Gotcha #4: TXT Record Quoting#

TXT records are used for SPF, DKIM verification. The format is "v=spf1 include:_spf.google.com ~all". The data field already includes quotes.

But there’s a catch: long TXT records (>255 chars) are split into multiple strings:

{ "data": "\"v=spf1\" \"include:_spf.google.com\" \"~all\"" }

Handle this by joining:

const formatTxtRecord = (data: string): string => {
  // Remove outer quotes, join multiple strings
  return data.replace(/" "/g, '').replace(/^"|"$/g, '')
}

Performance: Query Time Tracking#

DNS query time is useful for diagnosing network conditions. Use performance.now() instead of Date.now() for higher precision:

const start = performance.now()
const response = await fetch(...)
const elapsed = Math.round(performance.now() - start)

performance.now() has microsecond precision, while Date.now() only has millisecond precision. For fast operations like DNS queries, precision matters.

History Implementation#

Query history is a practical feature. Manage it with React state:

interface HistoryEntry {
  domain: string
  type: RecordType
  timestamp: number
}

const [history, setHistory] = useState<HistoryEntry[]>([])

// Update history after successful query
setHistory((prev) => {
  const filtered = prev.filter(
    (h) => !(h.domain === trimmed && h.type === recordType)
  )
  return [{ domain: trimmed, type: recordType, timestamp: Date.now() }, ...filtered].slice(0, 10)
})

Deduplication: if the same domain+type combination exists, remove the old one first, then add the new one to the top. Limit to 10 entries to prevent unbounded growth.

CORS Considerations#

Google DNS API supports CORS, so you can call it directly from the browser:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET

Cloudflare’s API requires setting Accept: application/dns-json header, otherwise it returns 415 error. If you hit CORS issues:

  1. Use a CORS proxy (like cors-anywhere)
  2. Self-host a backend proxy
  3. Switch to a CORS-friendly API

Practical Use Cases#

This DNS lookup tool is useful during development:

  1. Check DNS resolution - Confirm domain resolves to expected IP
  2. Verify MX records - Debug email delivery issues
  3. View TXT records - Verify SPF, DKIM configuration
  4. Check CNAME - Confirm alias configuration
  5. Compare different DNS - Switch record types to compare results

Summary#

Building a frontend DNS lookup tool comes down to choosing the right API. Google DNS API returns JSON, making it frontend-friendly, but watch out for:

  • Strict domain validation regex
  • Type code to readable name mapping
  • Special formats for MX, TXT records
  • Use performance.now() for query timing
  • Deduplication and limit for history

Try the complete DNS lookup tool at JsonKit DNS Lookup.


Related Tools: