Email for your domain, with DNS setup handled. PostScale

    Sign Up

    Manage DNS with the TypeScript SDK

    7 min readIntermediate

    Quick answer

    Install @dnscale/dnscale on Node.js 20.19+, use ES modules, and set DNSCALE_API_KEY. The SDK provides typed zone and record helpers, async pagination, AbortSignal support, and structured errors. Keep the record ID returned after an update. Safe reads can retry; DNS changes are never automatically retried.

    What you'll learn
    • Install the published npm package and configure a TypeScript project
    • Authenticate with a scoped API key and list zones and records
    • Create a zone and manage a disposable TXT record
    • Control deadlines and handle API errors and rate limits
    • Use the full typed API and call the SDK from JavaScript

    The DNScale TypeScript SDK lets Node.js services, scripts, and background jobs manage DNS through typed requests. This guide uses the published 1.0.0 release of @dnscale/dnscale on npm. Source and issues are available in dnscaleou/dnscale-typescript.

    Start with a read-only inventory, then try zone creation and a disposable record lifecycle. The same client also works in plain JavaScript.

    Set up a TypeScript project

    You need Node.js 20.19 or newer, a DNScale account, and an API key. Create an example project:

    mkdir dnscale-typescript-example
    cd dnscale-typescript-example
    npm init -y
    npm pkg set type=module
    npm install @dnscale/dnscale@1.0.0
    npm install --save-dev typescript@5.9.3 @types/node@22
    mkdir src

    The SDK uses ES modules and includes its own TypeScript declarations. The development dependencies compile the examples and provide Node.js types. Commit package.json and package-lock.json with your application to retain the selected dependency versions.

    Create tsconfig.json:

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "strict": true,
        "rootDir": "src",
        "outDir": "dist"
      },
      "include": ["src/**/*.ts"]
    }

    Each TypeScript example below is a separate program. Save the example you want to run as src/index.ts, replacing the previous example, then compile and run it:

    npx tsc -p tsconfig.json && node dist/index.js

    Authenticate and choose scopes

    Create a key in the dashboard and supply it through the DNSCALE_API_KEY environment variable using your local environment or secret manager. new DNScale() reads that variable automatically. You can also pass apiKey in the constructor when loading a key from your application's secret store.

    TaskAPI scopes
    List or read zoneszones:read
    List or read recordszones:read, records:read
    Create, update, or delete recordszones:read, records:write
    Create, update, or delete zoneszones:read, zones:write
    Read account usageusage:read

    Zone and record-name restrictions still apply. Use a key limited to the resources your job needs; creating a new zone requires appropriate zone-creation permissions. See API authentication and scopes.

    Keep API keys in server-side code and out of browser bundles, source control, and logs. In a web application, call DNScale from a backend route or background worker.

    The default endpoint is https://api.dnscale.eu/v1. If you set the client's baseUrl for another environment, include /v1.

    List zones and records

    This read-only inventory needs zones:read and records:read. The shared signal limits the whole inventory to one minute:

    import { DNScale } from "@dnscale/dnscale";
     
    const dns = new DNScale();
    const signal = AbortSignal.timeout(60_000);
     
    for await (const zone of dns.zones.iter({ pageSize: 100, signal })) {
      console.log("Zone:", zone.id, zone.name);
     
      for await (const record of dns.records.iter(zone.id, { pageSize: 100, signal })) {
        console.log(record.id, record.name, record.type, record.content);
      }
    }

    iter fetches additional pages as you consume them. Both iterators accept page sizes from 1 to 100; breaking out of a loop stops additional page requests. Errors reject the iteration and can be handled with try/catch.

    For a single page, use list:

    import { DNScale } from "@dnscale/dnscale";
     
    const dns = new DNScale();
    const page = await dns.zones.list({ limit: 25, offset: 0 });
     
    console.log("Total zones:", page.pagination.total);
    for (const zone of page.zones) {
      console.log(zone.id, zone.name);
    }

    dns.records.list(zoneId, { limit: 25, offset: 0 }) returns records and pagination in the same way. Offset pagination is not an atomic snapshot: other processes can change the resources while you list them.

    Create a zone

    Set DNSCALE_ZONE_NAME to a domain you control and intend to add to your account. This example needs zones:read and zones:write and leaves the new zone in your account:

    import { DNScale } from "@dnscale/dnscale";
     
    const name = process.env.DNSCALE_ZONE_NAME;
    if (!name) throw new Error("Set DNSCALE_ZONE_NAME to the domain you want to add");
     
    const dns = new DNScale();
    const zone = await dns.zones.create({ name, region: "EU", type: "master" });
     
    console.log("Created zone:", zone.id, zone.name);

    Creating a zone does not change your registrar's nameserver delegation. Use DNS delegation for DNScale regions for nameserver details, and the zones API for zone settings.

    Create, read, update, and delete a TXT record

    Set DNSCALE_ZONE_ID to the ID of a test zone. The key needs zones:read, records:read, and records:write. This program creates a uniquely named TXT record, reads it, changes its content, and deletes the updated record. These are real DNS changes in the selected zone.

    import { randomUUID } from "node:crypto";
    import { DNScale, type CreateRecord } from "@dnscale/dnscale";
     
    const zoneId = process.env.DNSCALE_ZONE_ID;
    if (!zoneId) throw new Error("Set DNSCALE_ZONE_ID to a test zone ID");
     
    const dns = new DNScale();
    const input = {
      name: `_sdk-demo-${randomUUID()}`,
      type: "TXT",
      content: "sdk-demo-first",
      ttl: 300,
    } satisfies CreateRecord;
     
    const created = await dns.records.create(zoneId, input);
    console.log("Created:", created.id, created.name);
     
    const fetched = await dns.records.get(zoneId, created.id);
    console.log("Read:", fetched.content);
     
    const updated = await dns.records.update(zoneId, created.id, {
      ...input,
      content: "sdk-demo-second",
    });
    console.log("Updated:", updated.id, updated.content);
     
    await dns.records.delete(zoneId, updated.id);
    console.log("Deleted:", updated.id);

    Record updates take a complete record body, including name, type, and content. Record IDs are content-derived: keep the ID returned by the update, as this example does before deletion. Treat IDs as opaque strings.

    satisfies CreateRecord checks the request shape during compilation. It does not validate arbitrary data loaded at runtime; the server still validates the request and your permissions.

    If an operation fails, the record may remain in the zone. Inspect the current state before retrying or cleaning it up, especially after a timeout. Write requests are never automatically retried.

    For multi-value record sets, updateByName accepts { content: oldValue } to select an existing value, and deleteByName accepts { content: value } to delete one value. Omitting content from deleteByName deletes the entire RRset. See the records API for selection rules and shared RRset comments.

    Set request deadlines and cancellation

    The default request timeout is ten seconds, including retries and retry waits. Configure the client and pass an AbortSignal to set a shorter deadline for a particular operation:

    import { DNScale } from "@dnscale/dnscale";
     
    const dns = new DNScale({
      timeoutMs: 10_000,
      maxRetries: 2,
      retryBackoffMs: 250,
    });
     
    const page = await dns.zones.list({
      limit: 10,
      signal: AbortSignal.timeout(5_000),
    });
    console.log(page.zones);

    Every convenience helper accepts request options containing signal. Use an AbortController and pass its signal when your application needs to cancel a job explicitly. The SDK uses native fetch connection pooling and has no close() method.

    By default, GET, HEAD, and OPTIONS can retry network failures and HTTP 408, 429, 500, 502, 503, and 504 responses up to twice. Set maxRetries: 0 to disable retries. Retry waits are bounded, and a Retry-After greater than 30 seconds is surfaced to the caller instead of being shortened. POST, PUT, PATCH, and DELETE are never automatically retried.

    Handle errors and rate limits

    Catch RateLimitError before APIError, because it extends APIError. HTTP failures throw; cancellation and transport failures retain native fetch error behavior.

    import {
      APIError,
      DNScale,
      ProtocolError,
      RateLimitError,
    } from "@dnscale/dnscale";
     
    try {
      const dns = new DNScale();
      const page = await dns.zones.list({ limit: 10 });
      console.log(page.zones);
    } catch (error) {
      process.exitCode = 1;
     
      if (error instanceof RateLimitError) {
        console.error("Rate limited:", error.message);
        console.error("Retry-After:", error.retryAfter);
        console.error("Request ID:", error.requestId);
      } else if (error instanceof APIError) {
        console.error(error.statusCode, error.code, error.message);
        console.error("Request ID:", error.requestId);
      } else if (error instanceof ProtocolError) {
        console.error("Unexpected API response:", error.message);
      } else if (
        error instanceof Error &&
        (error.name === "AbortError" || error.name === "TimeoutError")
      ) {
        console.error("Request cancelled or deadline exceeded");
      } else {
        throw error;
      }
    }

    APIError exposes statusCode, code, message, requestId, and optional details. RateLimitError.retryAfter contains the header value as a string, or null when absent; it can be a delay in seconds or an HTTP date. A surfaced rate limit means the request could not complete within the configured retry policy. Schedule a later attempt according to your job's policy.

    ProtocolError covers invalid success envelopes and malformed resource or pagination data checked by the helpers. It is not full runtime schema validation. Missing credentials, invalid client options, network failures, and custom abort reasons may produce other errors; preserve them for your application's error handler.

    Use the full typed API

    dns.api exposes operations beyond the zone and record helpers, including DNSSEC, usage, billing, and account resources. This usage example needs usage:read:

    import { DNScale } from "@dnscale/dnscale";
     
    const dns = new DNScale();
    const { data } = await dns.api.GET("/usage/current", {
      signal: AbortSignal.timeout(5_000),
    });
     
    if (!data || data.status !== "success") {
      throw new Error("Expected a usage response");
    }
    console.log(data.data);

    The generated client returns the API's success envelope, so the payload is data.data. Convenience helpers such as zones.list() unwrap that envelope. Both surfaces share authentication, deadlines, retries, and the same throwing policy for HTTP errors.

    The package exports components, paths, and operations types, plus common aliases such as Zone, Record, and CreateRecord. Use the API reference for endpoint permissions and request fields.

    Use plain JavaScript

    TypeScript is optional. After installing the package, save this as list-zones.mjs and run node list-zones.mjs with a key that has zones:read:

    import { DNScale } from "@dnscale/dnscale";
     
    const dns = new DNScale();
    for await (const zone of dns.zones.iter()) {
      console.log(zone.id, zone.name);
    }

    The same methods, pagination, signals, and error classes work in JavaScript. Use ESM import syntax for this package.

    Next steps

    Frequently asked questions

    What is the DNScale package name on npm?
    The official package is @dnscale/dnscale. Version 1.0.0 is published on npm and includes TypeScript declarations.
    Which Node.js versions does the SDK support?
    The SDK requires Node.js 20.19 or newer and uses ES modules. Set type to module in package.json for a TypeScript project, or use .mjs files for JavaScript.
    Can I use the SDK in a browser?
    Use this SDK in server-side Node.js code. Keep your DNScale API key in a server environment or secret store, never in a browser bundle.
    How do I paginate through zones and records?
    Use for await...of with dns.zones.iter() or dns.records.iter(zoneId). Both accept pageSize values from 1 to 100 and an optional AbortSignal.
    Does the SDK automatically retry DNS changes?
    No. Only GET, HEAD, and OPTIONS requests can retry automatically. After a failed or timed-out write, inspect the current state before repeating the operation.
    Why did a record ID change after an update?
    DNScale record IDs are content-derived. Treat them as opaque strings and use the ID returned by an update for subsequent reads, updates, or deletion.

    Put the guide into practice

    Manage authoritative DNS records, review changes, and monitor your zones with DNScale.

    Start free