Build an SPF, DKIM and DMARC Checker API in Node.js
Yes—you can build an SPF, DKIM and DMARC checker API in Node.js by accepting a domain, querying its DNS records, and returning structured results with clear limitations. The key is to distinguish checking whether records exist and parse correctly from proving that a real email will pass authentication.
What the API should check
SPF, DKIM and DMARC are related email-authentication mechanisms, but they answer different questions. SPF publishes which sending hosts are authorized for a domain. DKIM lets a receiving server verify a cryptographic signature attached to a message. DMARC tells receivers how to handle messages that fail its alignment requirements and where to send reports.
A domain-only endpoint can inspect published DNS policy, but it cannot fully test DKIM or DMARC alignment for a particular message. DKIM verification needs the message’s signature and corresponding public key; DMARC evaluation also depends on the visible From domain and whether SPF or DKIM passes with an aligned domain. Make that distinction explicit in API responses.
A useful initial response might include each mechanism’s status, discovered record, parsed fields, warnings, and a note about what the check did not verify. Avoid a single “secure” or “valid” label: a syntactically valid record may still be too permissive, incomplete, or inconsistent with the domain’s sending setup.
DNS records and lookup names
SPF is published as a TXT record at the domain being checked, such as example.com. The record begins with v=spf1. A basic policy might look like v=spf1 include:_spf.example.net -all; the mechanisms and qualifiers determine which senders match. A domain should not publish multiple SPF records beginning with v=spf1 at the same name, because that creates a permerror for SPF evaluation.
DMARC is published as a TXT record at _dmarc.example.com, commonly beginning with v=DMARC1. Tags can specify a policy such as p=none, p=quarantine or p=reject, along with optional reporting and alignment settings. A monitoring policy is not the same as enforcement, and a DNS checker cannot determine whether an organization has reviewed its aggregate reports.
DKIM records are selector-specific. A message’s DKIM-Signature header contains a selector in the s= field and a signing domain in the d= field. The public key is usually looked up at selector._domainkey.signing-domain. Since a domain can use many selectors and DNS does not provide a standard list of all active selectors, an API generally needs a selector supplied by the caller or obtained from a message header.
Implementing DNS checks in Node.js
Node.js includes the node:dns/promises module, which provides asynchronous DNS queries without requiring a third-party DNS package. Query TXT records and handle each DNS outcome separately: a name with no matching record is different from a timeout, a resolver failure, or malformed record data.
import { resolveTxt } from 'node:dns/promises';
async function getTxtRecords(name) {
try {
const chunks = await resolveTxt(name);
return chunks.map(parts => parts.join(''));
} catch (error) {
if (error.code === 'ENODATA' || error.code === 'ENOTFOUND') return [];
throw error;
}
}
async function checkDomain(domain, selector) {
const [rootTxt, dmarcTxt, dkimTxt] = await Promise.all([
getTxtRecords(domain),
getTxtRecords(`_dmarc.${domain}`),
selector ? getTxtRecords(`${selector}._domainkey.${domain}`) : Promise.resolve(null)
]);
return {
spf: rootTxt.filter(record => /^v=spf1(?:\s|$)/i.test(record)),
dmarc: dmarcTxt.filter(record => /^v=DMARC1(?:\s|;|$)/i.test(record)),
dkim: dkimTxt
};
}
TXT answers can arrive as multiple character strings that together form one logical record, so join the chunks before parsing. The example is only a lookup starting point: production code should validate domain and selector inputs, normalize case where appropriate, parse tags carefully, and return controlled error categories rather than exposing raw resolver errors.
Designing a useful API response
Use an HTTP framework or Node’s built-in server to expose an endpoint such as GET /check?domain=example.com&selector=mail. Validate inputs before making DNS requests. Reject empty values, unreasonable lengths, malformed domain names, and selectors containing characters outside the format your implementation supports. Do not accept arbitrary DNS names or URL schemes without controls; otherwise the service can become a tool for probing internal or unintended targets.
Return JSON with stable fields and distinguish findings from lookup state. For example:
{
"domain": "example.com",
"checks": {
"spf": { "status": "found", "records": ["v=spf1 -all"] },
"dmarc": { "status": "not_found", "records": [] },
"dkim": { "status": "not_checked", "reason": "selector_required" }
}
}
Useful statuses include found, not_found, multiple_records, invalid, and lookup_error. Keep raw records alongside parsed interpretations so callers can inspect what DNS actually returned. Document that DNS changes may take time to appear consistently because resolvers cache answers according to TTLs.
Parsing, interpretation and common pitfalls
Finding a record is only the first step. SPF evaluation has a DNS lookup limit of ten mechanisms and modifiers that cause DNS queries during evaluation; exceeding it can produce a permerror. A simple parser can flag obvious issues, but accurate SPF evaluation must follow the specification’s evaluation rules, including nested includes, redirects, qualifiers, and DNS errors. Do not recursively query arbitrary records without limits or timeouts.
DMARC tags are semicolon-separated, and parsers should tolerate harmless whitespace while checking required fields and recognized values. Policy tags such as sp and alignment modes can affect interpretation. A syntactic check should not claim that a policy is operationally safe for a particular sender without knowing all legitimate mail sources.
For DKIM, a TXT lookup can confirm that a selector record appears to publish a key, but it cannot verify a message signature. Key records may be absent because the selector is wrong, retired, or associated with another signing domain. Do not label a missing key as proof that a domain has no DKIM: the API may simply not know which selector to query.
Reliability, safety and deployment
- Set DNS timeouts, request limits, and response-size limits; a slow resolver should not hold requests indefinitely.
- Cache DNS results for a bounded period that respects record TTLs where available, and make clear that cached results can lag behind DNS changes.
- Limit concurrent lookups and rate-limit callers to reduce abuse and protect service availability.
- Do not execute record contents or treat DNS text as trusted instructions. Escape output when displaying it in a web interface.
- Log operational failures without unnecessarily retaining submitted data, and avoid returning internal network details or stack traces to API users.
For a small service, Node’s built-in DNS resolver may be enough. A dedicated DNS library or DNS-over-HTTPS provider can offer different resolver controls or transport behavior, but it does not make the results authoritative or eliminate caching and propagation effects. Choose based on reliability, deployment constraints, and whether you need advanced DNS features.
Frequently Asked Questions
Can an API check SPF, DKIM and DMARC from a domain alone?
It can check SPF and DMARC records and can query DKIM only if it has a selector and signing domain. It cannot verify a message’s DKIM signature or fully evaluate DMARC alignment from the domain alone.
Why does a DKIM checker need a selector?
DKIM public keys are published under selector-specific names such as mail._domainkey.example.com. The selector is supplied in a signed message’s DKIM-Signature header; DNS does not provide a reliable standard way to enumerate every selector.
Does a valid SPF record mean email will pass SPF?
No. The sending IP must match the policy, and SPF evaluation can fail because of DNS errors, lookup limits, or other evaluation results. SPF also authenticates the envelope sender domain, not necessarily the visible From address.
Why do I get different DNS results at different times?
Resolvers cache answers, and DNS updates take time to propagate through caches. Network conditions and the resolver being queried can also affect whether a lookup succeeds at a particular moment.
Should the API call a domain secure when DMARC uses p=none?
No. p=none requests monitoring rather than quarantine or rejection. Report the published policy and its implications instead of reducing the result to a broad security label.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.