digitalocean-dns
v1.0.0
Published
A small, zero-dependency TypeScript SDK for managing DigitalOcean domains and DNS records.
Downloads
150
Maintainers
Readme
🌊 DigitalOcean DNS SDK
A zero-dependency TypeScript SDK for managing DigitalOcean Domains & DNS Records
Built for provisioning scripts and CI pipelines — create subdomains and records programmatically instead of clicking through the dashboard.
✨ Features
| Feature | Details |
|---------|---------|
| 🚫 Zero dependencies | Uses native fetch — nothing extra to install |
| 🔷 Fully typed | Records, inputs, responses — all with TypeScript generics |
| ♻️ Idempotent helpers | upsert* calls never duplicate records across re-runs |
| 📄 Auto pagination | All list* calls walk pages transparently |
| 🔁 Auto retry | HTTP 429 rate-limits are retried with Retry-After backoff |
| ⚡ Convenience setters | One-liner setters for A, AAAA, CNAME, TXT, MX |
📚 Table of Contents
- Requirements
- Installation
- Authentication
- Quick Start
- Client Construction
- Error Handling
- API Reference
- Types
- Full Example: Provisioning a Service
- Gotchas
📋 Requirements
Node 18+ is required. The SDK relies on the global
fetchandURLbuilt into Node 18. For older versions, pass afetchpolyfill via theoptionsobject — see Client Construction.
Add this to your tsconfig.json to ensure fetch types are in scope:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"types": ["node"],
"strict": true
}
}The package's bundled type declarations resolve automatically through its exports map under both NodeNext and Bundler module resolution.
📦 Installation
npm install digitalocean-dnsShips both ESM and CommonJS builds, plus bundled type declarations — works with import and require out of the box:
// ✅ ESM
import { DigitalOceanDNS } from "digitalocean-dns";// ✅ CommonJS
const { DigitalOceanDNS } = require("digitalocean-dns");🔑 Authentication
You need a DigitalOcean personal access token.
- Read scope → enough for
list*/get*methods - Write scope → required for create / update / delete calls
Steps:
- Open the DigitalOcean control panel → API → Tokens
- Generate a new token with the scopes you need
- Store it in an environment variable — never hardcode it
export DO_TOKEN="dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"🚀 Quick Start
import { DigitalOceanDNS } from "digitalocean-dns";
const dns = new DigitalOceanDNS(process.env.DO_TOKEN!);
// 📍 Point api.example.com at a server — safe to run repeatedly
await dns.setA("example.com", "api", "203.0.113.10", { ttl: 3600 });
// 🔗 Alias www → apex
await dns.setCNAME("example.com", "www", "example.com");
// 📋 Read it back
const records = await dns.listRecords("example.com", { type: "A" });
console.log(records);🔧 Client Construction
const dns = new DigitalOceanDNS(token, options?);| Param | Type | Description |
|-------|------|-------------|
| token | string (required) | DigitalOcean personal access token |
| options | DigitalOceanDNSOptions | Optional configuration (see below) |
DigitalOceanDNSOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseUrl | string | https://api.digitalocean.com/v2 | Override the API base URL |
| fetch | typeof fetch | globalThis.fetch | Inject a custom fetch (handy for testing or Node <18) |
| maxRetries | number | 3 | Max retries on HTTP 429 (rate limited) |
| perPage | number | 200 | Page size used when listing |
const dns = new DigitalOceanDNS(process.env.DO_TOKEN!, {
maxRetries: 5,
perPage: 100,
});🛑 Error Handling
Any non-2xx response throws a DigitalOceanDNSError with three fields:
| Field | Type | Description |
|-------|------|-------------|
| status | number | HTTP status code (e.g. 404) |
| code | string | DigitalOcean error id (e.g. "not_found") |
| requestId | string | Quote this in support tickets |
import { DigitalOceanDNS, DigitalOceanDNSError } from "digitalocean-dns";
const dns = new DigitalOceanDNS(process.env.DO_TOKEN!);
try {
await dns.getDomain("does-not-exist.com");
} catch (err) {
if (err instanceof DigitalOceanDNSError) {
console.error(err.status); // 404
console.error(err.code); // "not_found"
console.error(err.requestId); // for DO support
} else {
throw err;
}
}💡 Rate-limited requests (HTTP
429) are retried automatically up tomaxRetries, honoring theRetry-Afterheader. The error surfaces only if all retries are exhausted.
📖 API Reference
🗂️ Domains
listDomains()
List every domain (zone) on the account. Handles pagination internally.
const domains = await dns.listDomains();
// → [{ name: "example.com", ttl: 1800, zone_file: "..." }, ...]Returns: Promise<Domain[]>
getDomain(domain)
Retrieve a single domain by name.
const zone = await dns.getDomain("example.com");
console.log(zone.ttl); // → 1800Returns: Promise<Domain> — throws DigitalOceanDNSError (404) if not found.
createDomain(name, ipAddress?)
Add a domain (zone) to your account. Optionally pass an IP to auto-create the apex A record in one call.
// Zone only:
await dns.createDomain("example.com");
// Zone + apex A record in one shot:
await dns.createDomain("example.com", "203.0.113.10");Returns: Promise<Domain>
⚠️ The domain must already be delegated to DigitalOcean's nameservers at your registrar for records to resolve. See Gotchas.
deleteDomain(domain)
Permanently delete a domain and all of its records. Irreversible.
await dns.deleteDomain("example.com");Returns: Promise<void>
📝 Records — Raw CRUD
listRecords(domain, filter?)
List records for a domain. Both filters are optional. name is matched client-side, accepting short labels ("api"), FQDNs ("api.example.com"), or "@" for the apex.
// All records:
const all = await dns.listRecords("example.com");
// Only A records:
const aRecords = await dns.listRecords("example.com", { type: "A" });
// A specific record by name + type:
const [api] = await dns.listRecords("example.com", { type: "A", name: "api" });
console.log(api?.id, api?.data);Returns: Promise<DomainRecord[]>
getRecord(domain, id)
Retrieve a single record by its numeric id.
const record = await dns.getRecord("example.com", 98765432);Returns: Promise<DomainRecord>
createRecord(domain, record)
Create a record from a raw RecordInput object. Use this for full control — round-robin A records, SRV, CAA, etc.
// Round-robin: two A records on the same name (upsert can't do this)
await dns.createRecord("example.com", { type: "A", name: "api", data: "203.0.113.10" });
await dns.createRecord("example.com", { type: "A", name: "api", data: "203.0.113.11" });
// SRV record:
await dns.createRecord("example.com", {
type: "SRV",
name: "_sip._tcp",
data: "sipserver.example.com.",
priority: 10,
weight: 5,
port: 5060,
ttl: 3600,
});
// CAA record:
await dns.createRecord("example.com", {
type: "CAA",
name: "@",
data: "letsencrypt.org.",
flags: 0,
tag: "issue",
});Returns: Promise<DomainRecord>
updateRecord(domain, id, patch)
Partially update a record by id (sends a PATCH). Only include the fields you want to change.
// Repoint an A record and bump its TTL:
await dns.updateRecord("example.com", 98765432, {
data: "203.0.113.99",
ttl: 600,
});Returns: Promise<DomainRecord>
deleteRecord(domain, id)
Delete a record by its numeric id.
await dns.deleteRecord("example.com", 98765432);Returns: Promise<void>
♻️ Idempotent Helpers
upsertRecord(domain, record)
Create-or-update a record matched on type + name. Safe to run repeatedly — the same call always converges to the same single record, so it will never pile up duplicates across deploys.
// First run creates it; every later run updates it in place.
await dns.upsertRecord("example.com", {
type: "A",
name: "api",
data: "203.0.113.10",
ttl: 3600,
});Returns: Promise<DomainRecord>
⚠️ Assumes one record per name+type. If more than one match exists (e.g. round-robin A records), it throws a
DigitalOceanDNSErrorwith codeambiguous_upsertrather than guessing which one to overwrite. UsecreateRecord/deleteRecordfor those cases.
deleteRecordByName(domain, name, type)
Delete all records matching a name + type. Returns how many were removed (0 if none matched).
const removed = await dns.deleteRecordByName("example.com", "api", "A");
console.log(`Removed ${removed} record(s)`);Returns: Promise<number>
⚡ Convenience Setters
All setters are idempotent (they call upsertRecord internally) and return Promise<DomainRecord>. setCNAME and setMX automatically append the required trailing dot to their target.
// 🔵 A record (IPv4)
await dns.setA("example.com", "api", "203.0.113.10", { ttl: 3600 });
// 🟣 AAAA record (IPv6)
await dns.setAAAA("example.com", "api", "2001:db8::1");
// 🔗 CNAME — trailing dot added automatically ("example.com" → "example.com.")
await dns.setCNAME("example.com", "www", "example.com");
// 📄 TXT — domain verification, SPF, DKIM, etc.
await dns.setTXT("example.com", "@", "v=spf1 include:_spf.google.com ~all");
// 📬 MX — priority defaults to 10, trailing dot added automatically
await dns.setMX("example.com", "@", "mail.example.com", { priority: 10 });| Method | Signature | Notes |
|--------|-----------|-------|
| setA | (domain, name, ip, { ttl? }) | IPv4 address |
| setAAAA | (domain, name, ipv6, { ttl? }) | IPv6 address |
| setCNAME | (domain, name, target, { ttl? }) | Auto trailing dot on target |
| setTXT | (domain, name, value, { ttl? }) | Raw text value |
| setMX | (domain, name, target, { priority?, ttl? }) | priority defaults to 10 |
🔷 Types
All types are exported from the package root.
type RecordType = "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "SRV" | "NS" | "CAA" | "SOA";
interface DomainRecord {
id: number;
type: RecordType;
name: string;
data: string;
priority: number | null;
port: number | null;
ttl: number | null;
weight: number | null;
flags: number | null;
tag: string | null;
}
interface Domain {
name: string;
ttl: number;
zone_file: string;
}
interface RecordInput {
type: RecordType;
name: string;
data: string;
priority?: number | null;
port?: number | null;
ttl?: number | null;
weight?: number | null;
flags?: number | null;
tag?: string | null;
}
interface DigitalOceanDNSOptions {
baseUrl?: string;
fetch?: typeof fetch;
maxRetries?: number;
perPage?: number;
}
interface RecordOptions {
ttl?: number;
}
interface MXOptions extends RecordOptions {
priority?: number;
}🏗️ Full Example: Provisioning a Service
A realistic, re-runnable provisioning script — bring a subdomain online with web, API, and mail records, then verify.
import { DigitalOceanDNS, DigitalOceanDNSError } from "digitalocean-dns";
const dns = new DigitalOceanDNS(process.env.DO_TOKEN!);
const DOMAIN = "example.com";
const SERVER_IP = "203.0.113.10";
async function provision() {
// 🌐 Ensure the zone exists (ignore "already exists" 422)
try {
await dns.createDomain(DOMAIN, SERVER_IP);
} catch (err) {
if (!(err instanceof DigitalOceanDNSError && err.status === 422)) throw err;
}
// ♻️ Idempotent records — run this script as many times as you like
await dns.setA(DOMAIN, "@", SERVER_IP);
await dns.setA(DOMAIN, "api", SERVER_IP, { ttl: 600 });
await dns.setCNAME(DOMAIN, "www", DOMAIN);
await dns.setMX(DOMAIN, "@", "mail.example.com", { priority: 10 });
await dns.setTXT(DOMAIN, "@", "v=spf1 include:_spf.google.com ~all");
// ✅ Verify
const records = await dns.listRecords(DOMAIN);
console.table(
records.map((r) => ({ type: r.type, name: r.name, data: r.data, ttl: r.ttl }))
);
}
provision().catch((err) => {
console.error("❌ Provisioning failed:", err);
process.exit(1);
});⚠️ Gotchas
Keep these in mind to avoid surprises.
| # | Gotcha | Details |
|---|--------|---------|
| 1 | 🌐 Nameserver delegation is separate | This SDK manages records inside DigitalOcean's DNS. Your domain must already be pointed at ns1/ns2/ns3.digitalocean.com at your registrar, or nothing will resolve. The API can't change delegation at your registrar. |
| 2 | 🔴 CNAME / MX / NS targets need a trailing dot | setCNAME and setMX add it automatically. If you use createRecord directly, include it yourself: "target.example.com." |
| 3 | 🔢 Records are identified by numeric id, not name | To update/delete by name, list-and-filter first — which is exactly what upsertRecord and deleteRecordByName do for you. |
| 4 | ♻️ upsertRecord assumes one record per name+type | For round-robin (multiple A records on one name), use createRecord directly — upsert throws ambiguous_upsert if it finds more than one match. |
| 5 | 🏠 Creating a zone with an IP auto-creates the apex A record | If you then call setA(domain, "@", ...) with a different IP, it updates that same record — which is usually what you want. |
Made with ❤️ · MIT License · DigitalOcean v2 API Docs
