dns-providers

Implement DNS provider adapters for Cloudflare and AWS Route 53 in Node.js/TypeScript with a shared interface.

7 stars

Best use case

dns-providers is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Implement DNS provider adapters for Cloudflare and AWS Route 53 in Node.js/TypeScript with a shared interface.

Teams using dns-providers should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/dns-providers/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/devops-infrastructure/dns-record-manager/skills/dns-providers/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/dns-providers/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How dns-providers Compares

Feature / Agentdns-providersStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Implement DNS provider adapters for Cloudflare and AWS Route 53 in Node.js/TypeScript with a shared interface.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

## When to use this skill

Use this skill when implementing DNS provider adapters: Cloudflare API v4 via fetch, AWS Route 53 via the AWS SDK v3, or the shared `DnsProvider` interface.

## Provider Interface

```typescript
interface DnsRecord {
  id: string;
  zoneId: string;
  name: string;            // FQDN
  type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SRV' | 'CAA';
  content: string;
  ttl: number;
  priority?: number;
  proxied?: boolean;       // Cloudflare only
  comment?: string;
}

interface DnsZone {
  id: string;
  name: string;
  provider: string;
  status?: string;
}

interface DnsProvider {
  name: string;
  listZones(): Promise<DnsZone[]>;
  listRecords(zoneId: string): Promise<DnsRecord[]>;
  createRecord(zoneId: string, record: Omit<DnsRecord, 'id'>): Promise<DnsRecord>;
  updateRecord(zoneId: string, recordId: string, record: Partial<DnsRecord>): Promise<DnsRecord>;
  deleteRecord(zoneId: string, recordId: string): Promise<void>;
}
```

## Cloudflare Adapter

```typescript
import { z } from 'zod';

const CF_BASE = 'https://api.cloudflare.com/client/v4';

export class CloudflareProvider implements DnsProvider {
  name = 'cloudflare';

  constructor(private token: string, private allowedZoneIds?: string[]) {}

  private async req<T>(path: string, options?: RequestInit): Promise<T> {
    const res = await fetch(`${CF_BASE}${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${this.token}`,
        'Content-Type': 'application/json',
        ...options?.headers,
      },
    });
    const json = (await res.json()) as { success: boolean; result: T; errors: { message: string }[] };
    if (!json.success) {
      throw new Error(json.errors.map((e) => e.message).join(', '));
    }
    return json.result;
  }

  async listZones(): Promise<DnsZone[]> {
    const zones = await this.req<Array<{ id: string; name: string; status: string }>>('/zones?per_page=50');
    return zones
      .filter((z) => !this.allowedZoneIds || this.allowedZoneIds.includes(z.id))
      .map((z) => ({ id: z.id, name: z.name, provider: 'cloudflare', status: z.status }));
  }

  async listRecords(zoneId: string): Promise<DnsRecord[]> {
    const records = await this.req<Array<Record<string, unknown>>>(`/zones/${zoneId}/dns_records?per_page=1000`);
    return records.map((r) => ({
      id: r.id as string,
      zoneId,
      name: r.name as string,
      type: r.type as DnsRecord['type'],
      content: r.content as string,
      ttl: r.ttl as number,
      priority: r.priority as number | undefined,
      proxied: r.proxied as boolean | undefined,
      comment: r.comment as string | undefined,
    }));
  }

  async createRecord(zoneId: string, record: Omit<DnsRecord, 'id'>): Promise<DnsRecord> {
    const result = await this.req<Record<string, unknown>>(`/zones/${zoneId}/dns_records`, {
      method: 'POST',
      body: JSON.stringify(record),
    });
    return { ...record, id: result.id as string };
  }

  async updateRecord(zoneId: string, recordId: string, record: Partial<DnsRecord>): Promise<DnsRecord> {
    const result = await this.req<Record<string, unknown>>(
      `/zones/${zoneId}/dns_records/${recordId}`,
      { method: 'PATCH', body: JSON.stringify(record) },
    );
    return result as unknown as DnsRecord;
  }

  async deleteRecord(zoneId: string, recordId: string): Promise<void> {
    await this.req(`/zones/${zoneId}/dns_records/${recordId}`, { method: 'DELETE' });
  }
}
```

## Cloudflare TTL Values

| Value | Meaning |
|-------|---------|
| `1` | Auto (proxied records only) |
| `60` | 1 minute |
| `300` | 5 minutes |
| `3600` | 1 hour |
| `86400` | 1 day |

## Route 53 Adapter

```typescript
import {
  Route53Client,
  ListHostedZonesCommand,
  ListResourceRecordSetsCommand,
  ChangeResourceRecordSetsCommand,
  ChangeAction,
} from '@aws-sdk/client-route-53';

export class Route53Provider implements DnsProvider {
  name = 'route53';
  private client: Route53Client;

  constructor(region: string, private allowedZoneIds?: string[]) {
    this.client = new Route53Client({ region });
  }

  async listZones(): Promise<DnsZone[]> {
    const { HostedZones = [] } = await this.client.send(new ListHostedZonesCommand({}));
    return HostedZones.filter((z) => {
      const id = z.Id?.replace('/hostedzone/', '') ?? '';
      return !this.allowedZoneIds || this.allowedZoneIds.includes(id);
    }).map((z) => ({
      id: z.Id?.replace('/hostedzone/', '') ?? '',
      name: z.Name?.replace(/\.$/, '') ?? '',
      provider: 'route53',
    }));
  }

  async listRecords(zoneId: string): Promise<DnsRecord[]> {
    const { ResourceRecordSets = [] } = await this.client.send(
      new ListResourceRecordSetsCommand({ HostedZoneId: zoneId }),
    );

    return ResourceRecordSets.flatMap((rrs) =>
      (rrs.ResourceRecords ?? []).map((rr) => ({
        id: `${rrs.Name}-${rrs.Type}`,
        zoneId,
        name: rrs.Name?.replace(/\.$/, '') ?? '',
        type: rrs.Type as DnsRecord['type'],
        content: rr.Value ?? '',
        ttl: rrs.TTL ?? 300,
        priority: rrs.Type === 'MX' ? parseInt(rr.Value?.split(' ')[0] ?? '0', 10) : undefined,
      })),
    );
  }

  async createRecord(zoneId: string, record: Omit<DnsRecord, 'id'>): Promise<DnsRecord> {
    await this.client.send(new ChangeResourceRecordSetsCommand({
      HostedZoneId: zoneId,
      ChangeBatch: {
        Changes: [{
          Action: ChangeAction.CREATE,
          ResourceRecordSet: {
            Name: record.name,
            Type: record.type,
            TTL: record.ttl,
            ResourceRecords: [{ Value: record.content }],
          },
        }],
      },
    }));
    return { ...record, id: `${record.name}-${record.type}` };
  }

  async deleteRecord(zoneId: string, recordId: string): Promise<void> {
    // Route 53 requires the full record to delete -- fetch first
    const records = await this.listRecords(zoneId);
    const rec = records.find((r) => r.id === recordId);
    if (!rec) throw new Error(`Record ${recordId} not found`);

    await this.client.send(new ChangeResourceRecordSetsCommand({
      HostedZoneId: zoneId,
      ChangeBatch: {
        Changes: [{
          Action: ChangeAction.DELETE,
          ResourceRecordSet: {
            Name: rec.name,
            Type: rec.type,
            TTL: rec.ttl,
            ResourceRecords: [{ Value: rec.content }],
          },
        }],
      },
    }));
  }

  async updateRecord(zoneId: string, recordId: string, updates: Partial<DnsRecord>): Promise<DnsRecord> {
    const records = await this.listRecords(zoneId);
    const existing = records.find((r) => r.id === recordId);
    if (!existing) throw new Error(`Record ${recordId} not found`);

    await this.deleteRecord(zoneId, recordId);
    const updated = { ...existing, ...updates };
    return this.createRecord(zoneId, updated);
  }
}
```

## Provider Factory

```typescript
export function createProviders(env: NodeJS.ProcessEnv): DnsProvider[] {
  const providers: DnsProvider[] = [];

  if (env.CLOUDFLARE_API_TOKEN) {
    const zoneIds = env.CLOUDFLARE_ZONE_IDS?.split(',').map((s) => s.trim()).filter(Boolean);
    providers.push(new CloudflareProvider(env.CLOUDFLARE_API_TOKEN, zoneIds));
  }

  if (env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY) {
    const zoneIds = env.ROUTE53_HOSTED_ZONE_IDS?.split(',').map((s) => s.trim()).filter(Boolean);
    providers.push(new Route53Provider(env.AWS_REGION ?? 'us-east-1', zoneIds));
  }

  if (env.STATIC_ZONES_FILE) {
    providers.push(new StaticProvider(env.STATIC_ZONES_FILE));
  }

  return providers;
}
```

## Zone ID Encoding

Cloudflare zone IDs are alphanumeric strings (e.g. `abc123def456`).
Route 53 hosted zone IDs are returned as `/hostedzone/Z1234567890AB` -- strip the prefix when storing or passing as a parameter.

## Error Handling

| Provider | Error Class | Message Pattern |
|----------|-------------|-----------------|
| Cloudflare | Throws from `req()` | Cloudflare API error codes (9xxx) |
| Route 53 | AWS SDK throws `ServiceException` | HTTP status + message |
| Both | Wrap in provider-agnostic `Error` before returning to API layer |

Related Skills

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Pastebin Core

7
from heldernoid/agentic-build-templates

## Purpose