Repository: favonia/cloudflare-ddns
Pull request #208 (2022-08-16 12:56:48)

refactor: factor out domain from api

codecov[bot]: # [Codecov](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
> Merging [#208](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (ac342f5) into [main](https://codecov.io/gh/favonia/cloudflare-ddns/commit/b973a1496364622a735ed4415db8d17827f8a864?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (b973a14) will **decrease** coverage by `0.09%`.
> The diff coverage is `93.10%`.

> :exclamation: Current head ac342f5 differs from pull request most recent head fc0ee93. Consider uploading reports for the commit fc0ee93 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #208      +/-   ##
==========================================
- Coverage   83.60%   83.51%   -0.10%     
==========================================
  Files          27       27              
  Lines        1690     1705      +15     
==========================================
+ Hits         1413     1424      +11     
- Misses        268      271       +3     
- Partials        9       10       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `83.51% <93.10%> (-0.10%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [internal/setter/setter.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvc2V0dGVyL3NldHRlci5nbw==) | `96.46% <88.23%> (-3.54%)` | :arrow_down: |
| [internal/api/cloudflare.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvYXBpL2Nsb3VkZmxhcmUuZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/config/config.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2NvbmZpZy5nbw==) | `97.86% <100.00%> (ø)` | |
| [internal/config/env.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2Vudi5nbw==) | `98.46% <100.00%> (ø)` | |
| [internal/domain/domain.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL2RvbWFpbi5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/domain/fqdn.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL2ZxZG4uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/domain/wildcard.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL3dpbGRjYXJkLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/updater/updater.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/208/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvdXBkYXRlci91cGRhdGVyLmdv) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)


Patch:
diff --git a/internal/README.markdown b/internal/README.markdown
new file mode 100644
index 0000000..ddf7dda
--- /dev/null
+++ b/internal/README.markdown
@@ -0,0 +1,13 @@
+# Internal Libraries
+
+- `api`: access and cache DNS service API, currently only supporting Cloudflare
+- `config`: read configuration settings from environment variables
+- `cron`: parse Cron expressions
+- `domain`: handle domain names and split them into possible subdomains and zones
+- `file`: virtualize file system (to enable testing)
+- `ipnet`: define a type for labelling IPv4 and IPv6
+- `monitor`: ping the monitoring API, currently only supporting Healthchecks.io
+- `pp`: pretty print messages with emojis
+- `provider`: find out the public IP
+- `setter`: set the IP of one domain using a DNS service API
+- `updater`: detect and update the IP of all domains, using `provider` and `setter`
diff --git a/internal/api/auth.go b/internal/api/auth.go
deleted file mode 100644
index b85632d..0000000
--- a/internal/api/auth.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package api
-
-import (
-	"context"
-	"time"
-
-	"github.com/favonia/cloudflare-ddns/internal/pp"
-)
-
-type Auth interface {
-	New(context.Context, pp.PP, time.Duration) (Handle, bool)
-}
diff --git a/internal/api/base.go b/internal/api/base.go
index b321505..67085f3 100644
--- a/internal/api/base.go
+++ b/internal/api/base.go
@@ -3,39 +3,32 @@ package api
 import (
 	"context"
 	"net/netip"
+	"time"
 
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-// A DomainSplitter enumerates all possible zones from a domain.
-type DomainSplitter interface {
-	// IsValid checks whether the current splitting point is still valid
-	IsValid() bool
-	// ZoneNameASCII gives the suffix (the zone), when it is still valid
-	ZoneNameASCII() string
-	// Next moves to the next possible splitting point, which might end up being invalid
-	Next()
-}
-
-// A Domain represents a domain name to update.
-type Domain interface {
-	// DNSNameASCII gives a name suitable for accessing the Cloudflare API
-	DNSNameASCII() string
-	// Describe gives the most human-readable domain name that is still unambiguous
-	Describe() string
-	// Split gives a DomainSplitter that can be used to find zones
-	Split() DomainSplitter
-}
-
 //go:generate mockgen -destination=../mocks/mock_api.go -package=mocks . Handle
 
-// Handle represents a generic API to update DNS records. Currently, the only implementation is Cloudflare.
+// A Handle represents a generic API to update DNS records. Currently, the only implementation is Cloudflare.
 type Handle interface {
-	ListRecords(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type) (map[string]netip.Addr, bool)
-	DeleteRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type, id string) bool
-	UpdateRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type, id string, ip netip.Addr) bool
-	CreateRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type,
+	// List DNS records.
+	ListRecords(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipNet ipnet.Type) (map[string]netip.Addr, bool)
+	// Delete one DNS record.
+	DeleteRecord(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipNet ipnet.Type, id string) bool
+	// Update one DNS record.
+	UpdateRecord(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipNet ipnet.Type, id string, ip netip.Addr) bool
+	// Create one DNS record.
+	CreateRecord(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipNet ipnet.Type,
 		ip netip.Addr, ttl TTL, proxied bool) (string, bool)
+	// Flush the API cache.
 	FlushCache()
 }
+
+// An Auth contains authentication information.
+type Auth interface {
+	// Use the authentication information to create a Handle.
+	New(context.Context, pp.PP, time.Duration) (Handle, bool)
+}
diff --git a/internal/api/cloudflare.go b/internal/api/cloudflare.go
index 04963c4..0d7e188 100644
--- a/internal/api/cloudflare.go
+++ b/internal/api/cloudflare.go
@@ -8,6 +8,7 @@ import (
 	"github.com/cloudflare/cloudflare-go"
 	"github.com/patrickmn/go-cache"
 
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
@@ -124,7 +125,7 @@ func (h *CloudflareHandle) ActiveZones(ctx context.Context, ppfmt pp.PP, name st
 	return ids, true
 }
 
-func (h *CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain Domain) (string, bool) {
+func (h *CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain domain.Domain) (string, bool) {
 	if id, found := h.cache.zoneOfDomain.Get(domain.DNSNameASCII()); found {
 		return id.(string), true //nolint:forcetypeassert
 	}
@@ -155,7 +156,7 @@ zoneSearch:
 }
 
 func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
-	domain Domain, ipNet ipnet.Type,
+	domain domain.Domain, ipNet ipnet.Type,
 ) (map[string]netip.Addr, bool) {
 	if rmap, found := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); found {
 		return rmap.(map[string]netip.Addr), true //nolint:forcetypeassert
@@ -191,7 +192,7 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
 }
 
 func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
-	domain Domain, ipNet ipnet.Type, id string,
+	domain domain.Domain, ipNet ipnet.Type, id string,
 ) bool {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
 	if !ok {
@@ -215,7 +216,7 @@ func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
 }
 
 func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
-	domain Domain, ipNet ipnet.Type, id string, ip netip.Addr,
+	domain domain.Domain, ipNet ipnet.Type, id string, ip netip.Addr,
 ) bool {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
 	if !ok {
@@ -246,7 +247,7 @@ func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
 }
 
 func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
-	domain Domain, ipNet ipnet.Type, ip netip.Addr, ttl TTL, proxied bool,
+	domain domain.Domain, ipNet ipnet.Type, ip netip.Addr, ttl TTL, proxied bool,
 ) (string, bool) {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
 	if !ok {
diff --git a/internal/config/config.go b/internal/config/config.go
index cef61ab..b4cad4c 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/file"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/monitor"
@@ -16,14 +17,14 @@ import (
 type Config struct {
 	Auth             api.Auth
 	Provider         map[ipnet.Type]provider.Provider
-	Domains          map[ipnet.Type][]api.Domain
+	Domains          map[ipnet.Type][]domain.Domain
 	UpdateCron       cron.Schedule
 	UpdateOnStart    bool
 	DeleteOnStop     bool
 	CacheExpiration  time.Duration
 	TTL              api.TTL
 	DefaultProxied   bool
-	ProxiedByDomain  map[api.Domain]bool
+	ProxiedByDomain  map[domain.Domain]bool
 	DetectionTimeout time.Duration
 	UpdateTimeout    time.Duration
 	Monitors         []monitor.Monitor
@@ -37,7 +38,7 @@ func Default() *Config {
 			ipnet.IP4: provider.NewCloudflareTrace(),
 			ipnet.IP6: provider.NewCloudflareTrace(),
 		},
-		Domains: map[ipnet.Type][]api.Domain{
+		Domains: map[ipnet.Type][]domain.Domain{
 			ipnet.IP4: nil,
 			ipnet.IP6: nil,
 		},
@@ -47,7 +48,7 @@ func Default() *Config {
 		CacheExpiration:  time.Hour * 6, //nolint:gomnd
 		TTL:              api.TTL(1),
 		DefaultProxied:   false,
-		ProxiedByDomain:  map[api.Domain]bool{},
+		ProxiedByDomain:  map[domain.Domain]bool{},
 		UpdateTimeout:    time.Second * 30, //nolint:gomnd
 		DetectionTimeout: time.Second * 5,  //nolint:gomnd
 		Monitors:         nil,
@@ -104,8 +105,8 @@ func ReadAuth(ppfmt pp.PP, field *api.Auth) bool {
 
 // deduplicate always sorts and deduplicates the input list,
 // returning true if elements are already distinct.
-func deduplicate(list []api.Domain) []api.Domain {
-	api.SortDomains(list)
+func deduplicate(list []domain.Domain) []domain.Domain {
+	domain.SortDomains(list)
 
 	if len(list) == 0 {
 		return list
@@ -127,8 +128,8 @@ func deduplicate(list []api.Domain) []api.Domain {
 	return list[:j+1]
 }
 
-func ReadDomainMap(ppfmt pp.PP, field *map[ipnet.Type][]api.Domain) bool {
-	var domains, ip4Domains, ip6Domains []api.Domain
+func ReadDomainMap(ppfmt pp.PP, field *map[ipnet.Type][]domain.Domain) bool {
+	var domains, ip4Domains, ip6Domains []domain.Domain
 
 	if !ReadDomains(ppfmt, "DOMAINS", &domains) ||
 		!ReadDomains(ppfmt, "IP4_DOMAINS", &ip4Domains) ||
@@ -139,7 +140,7 @@ func ReadDomainMap(ppfmt pp.PP, field *map[ipnet.Type][]api.Domain) bool {
 	ip4Domains = deduplicate(append(ip4Domains, domains...))
 	ip6Domains = deduplicate(append(ip6Domains, domains...))
 
-	*field = map[ipnet.Type][]api.Domain{
+	*field = map[ipnet.Type][]domain.Domain{
 		ipnet.IP4: ip4Domains,
 		ipnet.IP6: ip6Domains,
 	}
@@ -181,8 +182,8 @@ func ReadTTL(ppfmt pp.PP, field *api.TTL) bool {
 	return true
 }
 
-func ReadProxiedByDomain(ppfmt pp.PP, field *map[api.Domain]bool) bool {
-	var proxiedDomains, nonProxiedDomains []api.Domain
+func ReadProxiedByDomain(ppfmt pp.PP, field *map[domain.Domain]bool) bool {
+	var proxiedDomains, nonProxiedDomains []domain.Domain
 
 	if !ReadDomains(ppfmt, "PROXIED_DOMAINS", &proxiedDomains) ||
 		!ReadDomains(ppfmt, "NON_PROXIED_DOMAINS", &nonProxiedDomains) {
@@ -199,7 +200,7 @@ func ReadProxiedByDomain(ppfmt pp.PP, field *map[api.Domain]bool) bool {
 	}
 
 	// the new map to be created
-	m := map[api.Domain]bool{}
+	m := map[domain.Domain]bool{}
 
 	// all proxied domains
 	for _, proxiedDomain := range proxiedDomains {
@@ -223,7 +224,7 @@ func ReadProxiedByDomain(ppfmt pp.PP, field *map[api.Domain]bool) bool {
 	return true
 }
 
-func describeDomains(domains []api.Domain) string {
+func describeDomains(domains []domain.Domain) string {
 	if len(domains) == 0 {
 		return "(none)"
 	}
@@ -265,9 +266,9 @@ func (c *Config) Print(ppfmt pp.PP) {
 	ppfmt.Infof(pp.EmojiConfig, "New DNS records:")
 	inner.Infof(pp.EmojiBullet, "TTL:              %s", c.TTL.Describe())
 	{
-		proxiedMapping := map[bool][]api.Domain{}
-		proxiedMapping[true] = make([]api.Domain, 0, len(c.ProxiedByDomain))
-		proxiedMapping[false] = make([]api.Domain, 0, len(c.ProxiedByDomain))
+		proxiedMapping := map[bool][]domain.Domain{}
+		proxiedMapping[true] = make([]domain.Domain, 0, len(c.ProxiedByDomain))
+		proxiedMapping[false] = make([]domain.Domain, 0, len(c.ProxiedByDomain))
 		for domain, proxied := range c.ProxiedByDomain {
 			proxiedMapping[proxied] = append(proxiedMapping[proxied], domain)
 		}
@@ -343,7 +344,7 @@ func (c *Config) NormalizeDomains(ppfmt pp.PP) bool {
 	}
 
 	// domainSet is the set of managed domains.
-	domainSet := map[api.Domain]bool{}
+	domainSet := map[domain.Domain]bool{}
 	for ipNet, domains := range c.Domains {
 		if c.Provider[ipNet] != nil {
 			for _, domain := range domains {
@@ -367,7 +368,7 @@ func (c *Config) NormalizeDomains(ppfmt pp.PP) bool {
 
 	// fill in the default "proxied"
 	if c.ProxiedByDomain == nil {
-		c.ProxiedByDomain = map[api.Domain]bool{}
+		c.ProxiedByDomain = map[domain.Domain]bool{}
 		ppfmt.Warningf(pp.EmojiImpossible,
 			"Internal failure: ProxiedByDomain is re-initialized because it was nil",
 		)
diff --git a/internal/config/env.go b/internal/config/env.go
index 82f427a..5147a11 100644
--- a/internal/config/env.go
+++ b/internal/config/env.go
@@ -6,8 +6,8 @@ import (
 	"strings"
 	"time"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/monitor"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 	"github.com/favonia/cloudflare-ddns/internal/provider"
@@ -82,17 +82,17 @@ func ReadNonnegInt(ppfmt pp.PP, key string, field *int) bool {
 
 // ReadDomains reads an environment variable as a comma-separated list of domains.
 // Spaces are trimed.
-func ReadDomains(ppfmt pp.PP, key string, field *[]api.Domain) bool {
+func ReadDomains(ppfmt pp.PP, key string, field *[]domain.Domain) bool {
 	rawList := strings.Split(Getenv(key), ",")
 
-	*field = make([]api.Domain, 0, len(rawList))
+	*field = make([]domain.Domain, 0, len(rawList))
 	for _, item := range rawList {
 		item = strings.TrimSpace(item)
 		if item == "" {
 			continue
 		}
 
-		item, err := api.NewDomain(item)
+		item, err := domain.New(item)
 		if err != nil {
 			ppfmt.Warningf(pp.EmojiUserError, "Domain %q was added but it is ill-formed: %v", item.Describe(), err)
 		}
diff --git a/internal/domain/base.go b/internal/domain/base.go
new file mode 100644
index 0000000..fea78bb
--- /dev/null
+++ b/internal/domain/base.go
@@ -0,0 +1,21 @@
+package domain
+
+// A Splitter enumerates all possible zones from a domain.
+type Splitter interface {
+	// IsValid checks whether the current splitting point is still valid
+	IsValid() bool
+	// ZoneNameASCII gives the suffix (the zone), when it is still valid
+	ZoneNameASCII() string
+	// Next moves to the next possible splitting point, which might end up being invalid
+	Next()
+}
+
+// A Domain represents a domain name to update.
+type Domain interface {
+	// DNSNameASCII gives a name suitable for accessing the Cloudflare API
+	DNSNameASCII() string
+	// Describe gives the most human-readable domain name that is still unambiguous
+	Describe() string
+	// Split gives a Splitter that can be used to find zones
+	Split() Splitter
+}
diff --git a/internal/api/domain.go b/internal/domain/domain.go
similarity index 96%
rename from internal/api/domain.go
rename to internal/domain/domain.go
index 4ec28c1..4107255 100644
--- a/internal/api/domain.go
+++ b/internal/domain/domain.go
@@ -1,4 +1,4 @@
-package api
+package domain
 
 import (
 	"sort"
@@ -43,7 +43,7 @@ func safelyToUnicode(ascii string) (string, bool) {
 // the normalized domain in its Unicode form when the round trip
 // gives back the same ASCII form without errors. Otherwise,
 // the ASCII form (possibly using Punycode) is stored to avoid ambiguity.
-func NewDomain(domain string) (Domain, error) {
+func New(domain string) (Domain, error) {
 	normalized, err := profileDroppingLeadingDots.ToASCII(domain)
 
 	// Remove the final dot for consistency
diff --git a/internal/api/fqdn.go b/internal/domain/fqdn.go
similarity index 94%
rename from internal/api/fqdn.go
rename to internal/domain/fqdn.go
index 474400d..435a73c 100644
--- a/internal/api/fqdn.go
+++ b/internal/domain/fqdn.go
@@ -1,4 +1,4 @@
-package api
+package domain
 
 import "strings"
 
@@ -23,7 +23,7 @@ type FQDNSplitter struct {
 	exhausted bool
 }
 
-func (f FQDN) Split() DomainSplitter {
+func (f FQDN) Split() Splitter {
 	return &FQDNSplitter{
 		domain:    string(f),
 		cursor:    0,
diff --git a/internal/api/wildcard.go b/internal/domain/wildcard.go
similarity index 95%
rename from internal/api/wildcard.go
rename to internal/domain/wildcard.go
index 47fb81a..84ef9b1 100644
--- a/internal/api/wildcard.go
+++ b/internal/domain/wildcard.go
@@ -1,4 +1,4 @@
-package api
+package domain
 
 import "strings"
 
@@ -30,7 +30,7 @@ type WildcardSplitter struct {
 	exhausted bool
 }
 
-func (w Wildcard) Split() DomainSplitter {
+func (w Wildcard) Split() Splitter {
 	return &WildcardSplitter{
 		domain:    string(w),
 		cursor:    0,
diff --git a/internal/setter/base.go b/internal/setter/base.go
index 0b17640..3470078 100644
--- a/internal/setter/base.go
+++ b/internal/setter/base.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"net/netip"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
@@ -12,5 +12,5 @@ import (
 //go:generate mockgen -destination=../mocks/mock_setter.go -package=mocks . Setter
 
 type Setter interface {
-	Set(ctx context.Context, ppfmt pp.PP, Domain api.Domain, IPNetwork ipnet.Type, IP netip.Addr, proxied bool) bool
+	Set(ctx context.Context, ppfmt pp.PP, Domain domain.Domain, IPNetwork ipnet.Type, IP netip.Addr, proxied bool) bool
 }
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index 69164a1..b22c9f5 100644
--- a/internal/setter/setter.go
+++ b/internal/setter/setter.go
@@ -6,6 +6,7 @@ import (
 	"sort"
 
 	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
@@ -15,7 +16,8 @@ type setter struct {
 	TTL    api.TTL
 }
 
-func splitRecords(rmap map[string]netip.Addr, target netip.Addr) (matchedIDs, unmatchedIDs []string) {
+// partitionRecords partitions record maps into matched and unmatched ones.
+func partitionRecords(rmap map[string]netip.Addr, target netip.Addr) (matchedIDs, unmatchedIDs []string) {
 	if target.IsValid() {
 		for id, ip := range rmap {
 			if ip == target {
@@ -40,6 +42,7 @@ func splitRecords(rmap map[string]netip.Addr, target netip.Addr) (matchedIDs, un
 	return matchedIDs, unmatchedIDs
 }
 
+// New creates a new Setter.
 func New(_ppfmt pp.PP, handle api.Handle, ttl api.TTL) (Setter, bool) {
 	return &setter{
 		Handle: handle,
@@ -47,8 +50,10 @@ func New(_ppfmt pp.PP, handle api.Handle, ttl api.TTL) (Setter, bool) {
 	}, true
 }
 
+// Set calls the DNS service API to update the API of one domain.
+//
 //nolint:funlen,cyclop,gocognit
-func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain api.Domain, ipnet ipnet.Type, ip netip.Addr, proxied bool) bool { //nolint:lll
+func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipnet ipnet.Type, ip netip.Addr, proxied bool) bool { //nolint:lll
 	recordType := ipnet.RecordType()
 	domainDescription := domain.Describe()
 
@@ -59,47 +64,58 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain api.Domain, ipnet
 	}
 
 	// The intention of these two lists is to find or create a good record and then delete everything else.
-	matchedIDs, unmatchedIDs := splitRecords(rs, ip)
+	matchedIDs, unmatchedIDsToUpdate := partitionRecords(rs, ip)
 
-	// If ip is not valid, this should be vacuously true.
-	// If ip is valid, then we will check matchedIDs and unmatchedIDs, but before that,
-	// it is considered not up to date.
-	uptodate := !ip.IsValid()
+	// Whether the correct DNS record is already present
+	uptodate := false
 
-	// First, if there's a matched ID, use it and delete everything else.
-	// Note that this implies ip is valid, for otherwise uptodate would be true.
+	// If ip is not valid, it is considered "up to date", but we have to delete all existing records
+	if !ip.IsValid() {
+		// matchedIDs must be empty, due to how partitionRecords works
+		uptodate = true
+	}
+
+	// duplicateMatchedIDs are to be deleted; this is set when uptodate becomes true
+	var duplicateMatchedIDs []string
+
+	// If it's still not up to date, and there's a matched ID, use it and delete everything else.
+	// Note that this implies ip is valid, for otherwise uptodate would already be true.
 	if !uptodate && len(matchedIDs) > 0 {
 		uptodate = true
-		matchedIDs = matchedIDs[1:]
+		duplicateMatchedIDs = matchedIDs[1:]
 	}
 
-	// If it's up to date and there's nothing else, we are done!
-	if uptodate && len(matchedIDs) == 0 && len(unmatchedIDs) == 0 {
+	// If it's up to date and there are no other records, we are done!
+	if uptodate && len(duplicateMatchedIDs) == 0 && len(unmatchedIDsToUpdate) == 0 {
 		ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", recordType, domainDescription)
 		return true
 	}
 
 	// This counts the stale records that have not being deleted yet.
-	// We need a different variable (instead of checking len(unmatchedIDs) all the times)
-	// because when we fail to delete a record, we might remove it from unmatchedIDs,
-	// but that stale record should still be counted in numUndeletedUnmatched
-	numUndeletedUnmatched := len(unmatchedIDs)
+	// We need a different variable (instead of using len(unmatchedIDsToUpdate) all the times)
+	// because when we fail to delete a record, we will give up and remove that record from
+	// unmatchedIDsToUpdate, but that stale record should still be counted in numUndeletedUnmatched
+	// so that we know we have failed to complete the updating.
+	numUndeletedUnmatched := len(unmatchedIDsToUpdate)
 
 	// If somehow it's still not up to date, it means there are no matched records but ip is valid.
 	// This means we have to change a record or create a new one with the desired ip.
-	if !uptodate && ip.IsValid() {
+	if !uptodate {
+		// Temporary variable for the new unmatchedIDsToUpdate
 		var unhandled []string
 
 		// Let's go through all stale records
-		for i, id := range unmatchedIDs {
+		for i, id := range unmatchedIDsToUpdate {
 			// Let's try to update it first.
 			if s.Handle.UpdateRecord(ctx, ppfmt, domain, ipnet, id, ip) {
+				// If the updating succeeds, we can move on to the next stage!
 				ppfmt.Noticef(pp.EmojiUpdateRecord,
 					"Updated a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
 
+				// Now it's up to date! matchedIDs must be empty for otherwise uptodate would have been true
 				uptodate = true
 				numUndeletedUnmatched--
-				unhandled = unmatchedIDs[i+1:]
+				unhandled = unmatchedIDsToUpdate[i+1:]
 
 				break
 			} else {
@@ -107,29 +123,33 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain api.Domain, ipnet
 				if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
 					ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)",
 						recordType, domainDescription, id)
+
+					// Only when the deletion succeeds, we decrease the counter for undeleted incorrect records.
 					numUndeletedUnmatched--
 				}
+
 				// No matter whether the deletion succeeds, move on.
 				continue
 			}
 		}
 
-		unmatchedIDs = unhandled
+		unmatchedIDsToUpdate = unhandled
 	}
 
-	// If it's still not up to date, it means there are no stale records or that we fail to update one of them.
-	// The last resort is to create a new record with the correct ip.
-	// The checking "ip.IsValid()" is redundant but it does not hurt. (This function is too complicated.)
-	if !uptodate && ip.IsValid() {
+	// If it's still not up to date at this point, it means there are no stale records or that all attempts to
+	// update one of them failed. The last resort is to create a new record with the correct ip.
+	if !uptodate {
 		if id, ok := s.Handle.CreateRecord(ctx, ppfmt,
 			domain, ipnet, ip, s.TTL, proxied); ok {
 			ppfmt.Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", recordType, domainDescription, id)
+
+			// Now it's up to date! matchedIDs and unmatchedIDsToUpdate must both be empty at this point
 			uptodate = true
 		}
 	}
 
 	// Now, we should try to delete all remaining stale records.
-	for _, id := range unmatchedIDs {
+	for _, id := range unmatchedIDsToUpdate {
 		if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
 			ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
 			numUndeletedUnmatched--
@@ -137,14 +157,15 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain api.Domain, ipnet
 	}
 
 	// We should also delete all duplicate records even if they are up to date.
-	for _, id := range matchedIDs {
+	// This has lower priority than deleting the stale records.
+	for _, id := range duplicateMatchedIDs {
 		if s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
 			ppfmt.Noticef(pp.EmojiDelRecord, "Deleted a duplicate %s record of %q (ID: %s)",
 				recordType, domainDescription, id)
 		}
 	}
 
-	// It is okay to have duplicates, but it is not okay to have stale records.
+	// Check whether we are done. It is okay to have duplicates, but it is not okay to have (undeleted) stale records.
 	if !uptodate || numUndeletedUnmatched > 0 {
 		ppfmt.Errorf(pp.EmojiError,
 			"Failed to complete updating of %s records of %q; records might be inconsistent",
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 46720be..519ed31 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -4,14 +4,14 @@ import (
 	"context"
 	"net/netip"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 	"github.com/favonia/cloudflare-ddns/internal/setter"
 )
 
-func getProxied(ppfmt pp.PP, c *config.Config, domain api.Domain) bool {
+func getProxied(ppfmt pp.PP, c *config.Config, domain domain.Domain) bool {
 	if proxied, ok := c.ProxiedByDomain[domain]; ok {
 		return proxied
 	}


Test patch:
diff --git a/internal/api/cloudflare_test.go b/internal/api/cloudflare_test.go
index f7de84b..1838144 100644
--- a/internal/api/cloudflare_test.go
+++ b/internal/api/cloudflare_test.go
@@ -18,6 +18,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -344,18 +345,18 @@ func TestZoneOfDomain(t *testing.T) {
 
 	for name, tc := range map[string]struct {
 		zone          string
-		domain        api.Domain
+		domain        domain.Domain
 		zoneStatuses  map[string][]string
 		accessCount   int
 		expected      string
 		ok            bool
 		prepareMockPP func(*mocks.MockPP)
 	}{
-		"root":     {"test.org", api.FQDN("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil},     //nolint:lll
-		"wildcard": {"test.org", api.Wildcard("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil}, //nolint:lll
-		"one":      {"test.org", api.FQDN("sub.test.org"), map[string][]string{"test.org": {"active"}}, 2, mockID("test.org", 0), true, nil}, //nolint:lll
+		"root":     {"test.org", domain.FQDN("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil},     //nolint:lll
+		"wildcard": {"test.org", domain.Wildcard("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil}, //nolint:lll
+		"one":      {"test.org", domain.FQDN("sub.test.org"), map[string][]string{"test.org": {"active"}}, 2, mockID("test.org", 0), true, nil}, //nolint:lll
 		"none": {
-			"test.org", api.FQDN("sub.test.org"),
+			"test.org", domain.FQDN("sub.test.org"),
 			map[string][]string{},
 			3, "", false,
 			func(m *mocks.MockPP) {
@@ -363,7 +364,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"none/wildcard": {
-			"test.org", api.Wildcard("test.org"),
+			"test.org", domain.Wildcard("test.org"),
 			map[string][]string{},
 			2, "", false,
 			func(m *mocks.MockPP) {
@@ -371,7 +372,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"multiple": {
-			"test.org", api.FQDN("sub.test.org"),
+			"test.org", domain.FQDN("sub.test.org"),
 			map[string][]string{"test.org": {"active", "active"}},
 			2, "", false,
 			func(m *mocks.MockPP) {
@@ -383,7 +384,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"multiple/wildcard": {
-			"test.org", api.Wildcard("test.org"),
+			"test.org", domain.Wildcard("test.org"),
 			map[string][]string{"test.org": {"active", "active"}},
 			1, "", false,
 			func(m *mocks.MockPP) {
@@ -395,7 +396,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"deleted": {
-			"test.org", api.FQDN("test.org"),
+			"test.org", domain.FQDN("test.org"),
 			map[string][]string{"test.org": {"deleted"}},
 			2, "", false,
 			func(m *mocks.MockPP) {
@@ -406,7 +407,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"pending": {
-			"test.org", api.FQDN("test.org"),
+			"test.org", domain.FQDN("test.org"),
 			map[string][]string{"test.org": {"pending"}},
 			1, mockID("test.org", 0), true,
 			func(m *mocks.MockPP) {
@@ -417,7 +418,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"initializing": {
-			"test.org", api.FQDN("test.org"),
+			"test.org", domain.FQDN("test.org"),
 			map[string][]string{"test.org": {"initializing"}},
 			1, mockID("test.org", 0), true,
 			func(m *mocks.MockPP) {
@@ -428,7 +429,7 @@ func TestZoneOfDomain(t *testing.T) {
 			},
 		},
 		"undocumented": {
-			"test.org", api.FQDN("test.org"),
+			"test.org", domain.FQDN("test.org"),
 			map[string][]string{"test.org": {"some-undocumented-status"}},
 			1, mockID("test.org", 0), true,
 			func(m *mocks.MockPP) {
@@ -482,7 +483,7 @@ func TestZoneOfDomainInvalid(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	zoneID, ok := h.(*api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, api.FQDN("sub.test.org"))
+	zoneID, ok := h.(*api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, domain.FQDN("sub.test.org"))
 	require.False(t, ok)
 	require.Equal(t, "", zoneID)
 }
@@ -578,14 +579,14 @@ func TestListRecords(t *testing.T) {
 	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
 	ipNet, ips, accessCount = ipnet.IP6, expected, 1
 	mockPP := mocks.NewMockPP(mockCtrl)
-	ips, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, expected, ips)
 	require.Equal(t, 0, accessCount)
 
 	// testing the caching
 	mockPP = mocks.NewMockPP(mockCtrl)
-	ips, ok = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, expected, ips)
 }
@@ -636,7 +637,7 @@ func TestListRecordsInvalidIPAddress(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ips, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.False(t, ok)
 	require.Nil(t, ips)
 	require.Equal(t, 0, accessCount)
@@ -649,7 +650,7 @@ func TestListRecordsInvalidIPAddress(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ips, ok = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.False(t, ok)
 	require.Nil(t, ips)
 	require.Equal(t, 0, accessCount)
@@ -694,14 +695,14 @@ func TestListRecordsWildcard(t *testing.T) {
 	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
 	ipNet, ips, accessCount = ipnet.IP6, expected, 1
 	mockPP := mocks.NewMockPP(mockCtrl)
-	ips, ok := h.ListRecords(context.Background(), mockPP, api.Wildcard("test.org"), ipnet.IP6)
+	ips, ok := h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, expected, ips)
 	require.Equal(t, 0, accessCount)
 
 	// testing the caching
 	mockPP = mocks.NewMockPP(mockCtrl)
-	ips, ok = h.ListRecords(context.Background(), mockPP, api.Wildcard("test.org"), ipnet.IP6)
+	ips, ok = h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, expected, ips)
 }
@@ -717,13 +718,13 @@ func TestListRecordsInvalidDomain(t *testing.T) {
 
 	mockPP := mocks.NewMockPP(mockCtrl)
 	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
-	ips, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP4)
+	ips, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
 	require.False(t, ok)
 	require.Nil(t, ips)
 
 	mockPP = mocks.NewMockPP(mockCtrl)
 	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
-	ips, ok = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.False(t, ok)
 	require.Nil(t, ips)
 }
@@ -741,7 +742,7 @@ func TestListRecordsInvalidZone(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ips, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP4)
+	ips, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
 	require.False(t, ok)
 	require.Nil(t, ips)
 
@@ -752,7 +753,7 @@ func TestListRecordsInvalidZone(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ips, ok = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	ips, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.False(t, ok)
 	require.Nil(t, ips)
 }
@@ -826,14 +827,14 @@ func TestDeleteRecordValid(t *testing.T) {
 
 	deleteAccessCount = 1
 	mockPP := mocks.NewMockPP(mockCtrl)
-	ok := h.DeleteRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
 	require.True(t, ok)
 
 	listAccessCount, deleteAccessCount = 1, 1
 	mockPP = mocks.NewMockPP(mockCtrl)
-	_, _ = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
-	_ = h.DeleteRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1")
-	rs, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	_, _ = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	_ = h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	rs, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Empty(t, rs)
 }
@@ -854,7 +855,7 @@ func TestDeleteRecordInvalid(t *testing.T) {
 		"record1",
 		gomock.Any(),
 	)
-	ok := h.DeleteRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
 	require.False(t, ok)
 }
 
@@ -869,7 +870,7 @@ func TestDeleteRecordZoneInvalid(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ok := h.DeleteRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
 	require.False(t, ok)
 }
 
@@ -928,14 +929,14 @@ func TestUpdateRecordValid(t *testing.T) {
 
 	updateAccessCount = 1
 	mockPP := mocks.NewMockPP(mockCtrl)
-	ok := h.UpdateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
 	require.True(t, ok)
 
 	listAccessCount, updateAccessCount = 1, 1
 	mockPP = mocks.NewMockPP(mockCtrl)
-	_, _ = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
-	_ = h.UpdateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
-	rs, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	_, _ = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	_ = h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
+	rs, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::2")}, rs)
 }
@@ -956,7 +957,7 @@ func TestUpdateRecordInvalid(t *testing.T) {
 		"record1",
 		gomock.Any(),
 	)
-	ok := h.UpdateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
 	require.False(t, ok)
 }
 
@@ -971,7 +972,7 @@ func TestUpdateRecordInvalidZone(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	ok := h.UpdateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
 	require.False(t, ok)
 }
 
@@ -1031,15 +1032,15 @@ func TestCreateRecordValid(t *testing.T) {
 
 	createAccessCount = 1
 	mockPP := mocks.NewMockPP(mockCtrl)
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
 	require.True(t, ok)
 	require.Equal(t, "record1", actualID)
 
 	listAccessCount, createAccessCount = 1, 1
 	mockPP = mocks.NewMockPP(mockCtrl)
-	_, _ = h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
-	_, _ = h.CreateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
-	rs, ok := h.ListRecords(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6)
+	_, _ = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	_, _ = h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
+	rs, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
 	require.True(t, ok)
 	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::1")}, rs)
 }
@@ -1059,7 +1060,7 @@ func TestCreateRecordInvalid(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
 	require.False(t, ok)
 	require.Equal(t, "", actualID)
 }
@@ -1075,7 +1076,7 @@ func TestCreateRecordInvalidZone(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, api.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false) //nolint:lll
 	require.False(t, ok)
 	require.Equal(t, "", actualID)
 }
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index c788677..8948597 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/file"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
@@ -251,31 +252,31 @@ func TestReadDomainMap(t *testing.T) {
 		domains       string
 		ip4Domains    string
 		ip6Domains    string
-		expected      map[ipnet.Type][]api.Domain
+		expected      map[ipnet.Type][]domain.Domain
 		ok            bool
 		prepareMockPP func(*mocks.MockPP)
 	}{
 		"full": {
 			"  a1, a2", "b1,  b2,b2", "c1,c2",
-			map[ipnet.Type][]api.Domain{
-				ipnet.IP4: {api.FQDN("a1"), api.FQDN("a2"), api.FQDN("b1"), api.FQDN("b2")},
-				ipnet.IP6: {api.FQDN("a1"), api.FQDN("a2"), api.FQDN("c1"), api.FQDN("c2")},
+			map[ipnet.Type][]domain.Domain{
+				ipnet.IP4: {domain.FQDN("a1"), domain.FQDN("a2"), domain.FQDN("b1"), domain.FQDN("b2")},
+				ipnet.IP6: {domain.FQDN("a1"), domain.FQDN("a2"), domain.FQDN("c1"), domain.FQDN("c2")},
 			},
 			true,
 			nil,
 		},
 		"duplicate": {
 			"  a1, a1", "a1,  a1,a1", "*.a1,a1,*.a1,*.a1",
-			map[ipnet.Type][]api.Domain{
-				ipnet.IP4: {api.FQDN("a1")},
-				ipnet.IP6: {api.FQDN("a1"), api.Wildcard("a1")},
+			map[ipnet.Type][]domain.Domain{
+				ipnet.IP4: {domain.FQDN("a1")},
+				ipnet.IP6: {domain.FQDN("a1"), domain.Wildcard("a1")},
 			},
 			true,
 			nil,
 		},
 		"empty": {
 			" ", "   ", "",
-			map[ipnet.Type][]api.Domain{
+			map[ipnet.Type][]domain.Domain{
 				ipnet.IP4: {},
 				ipnet.IP6: {},
 			},
@@ -291,7 +292,7 @@ func TestReadDomainMap(t *testing.T) {
 			store(t, "IP4_DOMAINS", tc.ip4Domains)
 			store(t, "IP6_DOMAINS", tc.ip6Domains)
 
-			var field map[ipnet.Type][]api.Domain
+			var field map[ipnet.Type][]domain.Domain
 			mockPP := mocks.NewMockPP(mockCtrl)
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
@@ -383,22 +384,22 @@ func TestReadProxiedByDomain(t *testing.T) {
 		proxiedDomains    string
 		nonProxiedDomains string
 		ok                bool
-		expected          map[api.Domain]bool
+		expected          map[domain.Domain]bool
 		prepareMockPP     func(*mocks.MockPP)
 	}{
 		"empty": {
 			" ", "   ",
 			true,
-			map[api.Domain]bool{},
+			map[domain.Domain]bool{},
 			nil,
 		},
 		"both": {
 			"  a1, a2", "b1",
 			true,
-			map[api.Domain]bool{
-				api.FQDN("a1"): true,
-				api.FQDN("a2"): true,
-				api.FQDN("b1"): false,
+			map[domain.Domain]bool{
+				domain.FQDN("a1"): true,
+				domain.FQDN("a2"): true,
+				domain.FQDN("b1"): false,
 			},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -428,7 +429,7 @@ func TestReadProxiedByDomain(t *testing.T) {
 			store(t, "PROXIED_DOMAINS", tc.proxiedDomains)
 			store(t, "NON_PROXIED_DOMAINS", tc.nonProxiedDomains)
 
-			var field map[api.Domain]bool
+			var field map[domain.Domain]bool
 			mockPP := mocks.NewMockPP(mockCtrl)
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
@@ -543,8 +544,8 @@ func TestPrintDomains(t *testing.T) {
 
 	c := config.Default()
 
-	c.Domains[ipnet.IP4] = []api.Domain{api.FQDN("test4.org"), api.Wildcard("test4.org")}
-	c.Domains[ipnet.IP6] = []api.Domain{api.FQDN("test6.org"), api.Wildcard("test6.org")}
+	c.Domains[ipnet.IP4] = []domain.Domain{domain.FQDN("test4.org"), domain.Wildcard("test4.org")}
+	c.Domains[ipnet.IP6] = []domain.Domain{domain.FQDN("test6.org"), domain.Wildcard("test6.org")}
 
 	c.Print(mockPP)
 }
@@ -618,11 +619,11 @@ func TestPrintProxiedByDomain(t *testing.T) {
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Second*30),
 	)
 
-	c.ProxiedByDomain = map[api.Domain]bool{}
-	c.ProxiedByDomain[api.FQDN("a")] = true
-	c.ProxiedByDomain[api.FQDN("b")] = true
-	c.ProxiedByDomain[api.FQDN("c")] = false
-	c.ProxiedByDomain[api.FQDN("d")] = false
+	c.ProxiedByDomain = map[domain.Domain]bool{}
+	c.ProxiedByDomain[domain.FQDN("a")] = true
+	c.ProxiedByDomain[domain.FQDN("b")] = true
+	c.ProxiedByDomain[domain.FQDN("c")] = false
+	c.ProxiedByDomain[domain.FQDN("d")] = false
 	c.Print(mockPP)
 }
 
@@ -769,19 +770,19 @@ func TestNormalize(t *testing.T) {
 		},
 		"empty": {
 			input: &config.Config{ //nolint:exhaustruct
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			ok: false,
 			expected: &config.Config{ //nolint:exhaustruct
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -798,11 +799,11 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: provider.NewCloudflareTrace(),
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			ok: true,
 			expected: &config.Config{ //nolint:exhaustruct
@@ -810,12 +811,12 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: provider.NewCloudflareTrace(),
 					ipnet.IP6: nil,
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): false,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): false,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
@@ -835,11 +836,11 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			ok: false,
 			expected: &config.Config{ //nolint:exhaustruct
@@ -847,11 +848,11 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: nil,
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
@@ -871,11 +872,11 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c"), api.FQDN("d.e.f")},
-					ipnet.IP6: {api.FQDN("a.b.c"), api.FQDN("g.h.i")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c"), domain.FQDN("d.e.f")},
+					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("g.h.i")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{},
+				ProxiedByDomain: map[domain.Domain]bool{},
 			},
 			ok: true,
 			expected: &config.Config{ //nolint:exhaustruct
@@ -883,13 +884,13 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
-					ipnet.IP4: {api.FQDN("a.b.c"), api.FQDN("d.e.f")},
-					ipnet.IP6: {api.FQDN("a.b.c"), api.FQDN("g.h.i")},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP4: {domain.FQDN("a.b.c"), domain.FQDN("d.e.f")},
+					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("g.h.i")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): false,
-					api.FQDN("g.h.i"): false,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): false,
+					domain.FQDN("g.h.i"): false,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
@@ -909,9 +910,9 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
 				DefaultProxied:  true,
 				ProxiedByDomain: nil,
@@ -922,13 +923,13 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
 				DefaultProxied: true,
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): true,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): true,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
@@ -951,14 +952,14 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): true,
-					api.FQDN("d.e.f"): true,
-					api.FQDN("g.h.i"): false,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): true,
+					domain.FQDN("d.e.f"): true,
+					domain.FQDN("g.h.i"): false,
 				},
 			},
 			ok: true,
@@ -967,12 +968,12 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): true,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): true,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
@@ -993,12 +994,12 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): true,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): true,
 				},
 			},
 			ok: true,
@@ -1007,12 +1008,12 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
-				Domains: map[ipnet.Type][]api.Domain{
+				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: nil,
-					ipnet.IP6: {api.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[api.Domain]bool{
-					api.FQDN("a.b.c"): true,
+				ProxiedByDomain: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"): true,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
diff --git a/internal/config/env_test.go b/internal/config/env_test.go
index 0d235eb..4cb1237 100644
--- a/internal/config/env_test.go
+++ b/internal/config/env_test.go
@@ -9,9 +9,9 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
 	"github.com/favonia/cloudflare-ddns/internal/monitor"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -246,9 +246,9 @@ func TestReadNonnegInt(t *testing.T) {
 //nolint:paralleltest // environment vars are global
 func TestReadDomains(t *testing.T) {
 	key := keyPrefix + "DOMAINS"
-	type ds = []api.Domain
-	type f = api.FQDN
-	type w = api.Wildcard
+	type ds = []domain.Domain
+	type f = domain.FQDN
+	type w = domain.Wildcard
 	for name, tc := range map[string]struct {
 		set           bool
 		val           string
diff --git a/internal/api/domain_test.go b/internal/domain/domain_test.go
similarity index 92%
rename from internal/api/domain_test.go
rename to internal/domain/domain_test.go
index 9524766..ac7d89a 100644
--- a/internal/api/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -1,4 +1,4 @@
-package api_test
+package domain_test
 
 import (
 	"sort"
@@ -7,17 +7,17 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 )
 
 //nolint:funlen
-func TestNewDomain(t *testing.T) {
+func TestNew(t *testing.T) {
 	t.Parallel()
-	type f = api.FQDN
-	type w = api.Wildcard
+	type f = domain.FQDN
+	type w = domain.Wildcard
 	for _, tc := range [...]struct {
 		input     string
-		expected  api.Domain
+		expected  domain.Domain
 		ok        bool
 		errString string
 	}{
@@ -99,7 +99,7 @@ func TestNewDomain(t *testing.T) {
 		tc := tc
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
-			normalized, err := api.NewDomain(tc.input)
+			normalized, err := domain.New(tc.input)
 			require.Equal(t, tc.expected, normalized)
 			if tc.ok {
 				require.NoError(t, err)
@@ -115,8 +115,8 @@ func TestSortDomains(t *testing.T) {
 	t.Parallel()
 
 	require.NoError(t, quick.Check(
-		func(fs []api.FQDN, ws []api.Wildcard) bool {
-			merged := make([]api.Domain, 0, len(fs)+len(ws))
+		func(fs []domain.FQDN, ws []domain.Wildcard) bool {
+			merged := make([]domain.Domain, 0, len(fs)+len(ws))
 
 			for _, f := range fs {
 				merged = append(merged, f)
@@ -125,9 +125,9 @@ func TestSortDomains(t *testing.T) {
 				merged = append(merged, w)
 			}
 
-			copied := make([]api.Domain, len(merged))
+			copied := make([]domain.Domain, len(merged))
 			copy(copied, merged)
-			api.SortDomains(merged)
+			domain.SortDomains(merged)
 
 			require.ElementsMatch(t, copied, merged)
 			require.True(t, sort.SliceIsSorted(merged,
diff --git a/internal/api/fqdn_test.go b/internal/domain/fqdn_test.go
similarity index 89%
rename from internal/api/fqdn_test.go
rename to internal/domain/fqdn_test.go
index ec7c817..e4c249c 100644
--- a/internal/api/fqdn_test.go
+++ b/internal/domain/fqdn_test.go
@@ -1,4 +1,4 @@
-package api_test
+package domain_test
 
 import (
 	"testing"
@@ -6,7 +6,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 )
 
 func TestFQDNString(t *testing.T) {
@@ -14,7 +14,7 @@ func TestFQDNString(t *testing.T) {
 
 	require.NoError(t, quick.Check(
 		func(s string) bool {
-			return api.FQDN(s).DNSNameASCII() == s
+			return domain.FQDN(s).DNSNameASCII() == s
 		},
 		nil,
 	))
@@ -62,7 +62,7 @@ func TestFQDNDescribe(t *testing.T) {
 		tc := tc
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
-			require.Equal(t, tc.expected, api.FQDN(tc.input).Describe())
+			require.Equal(t, tc.expected, domain.FQDN(tc.input).Describe())
 		})
 	}
 }
@@ -85,7 +85,7 @@ func TestFQDNSplitter(t *testing.T) {
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
 			var rs []r
-			for s := api.FQDN(tc.input).Split(); s.IsValid(); s.Next() {
+			for s := domain.FQDN(tc.input).Split(); s.IsValid(); s.Next() {
 				rs = append(rs, s.ZoneNameASCII())
 			}
 			require.Equal(t, tc.expected, rs)
diff --git a/internal/api/wildcard_test.go b/internal/domain/wildcard_test.go
similarity index 87%
rename from internal/api/wildcard_test.go
rename to internal/domain/wildcard_test.go
index 06e7088..1abf0a0 100644
--- a/internal/api/wildcard_test.go
+++ b/internal/domain/wildcard_test.go
@@ -1,4 +1,4 @@
-package api_test
+package domain_test
 
 import (
 	"testing"
@@ -6,7 +6,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 )
 
 func TestWildcardString(t *testing.T) {
@@ -15,9 +15,9 @@ func TestWildcardString(t *testing.T) {
 	require.NoError(t, quick.Check(
 		func(s string) bool {
 			if s == "" {
-				return api.Wildcard(s).DNSNameASCII() == "*"
+				return domain.Wildcard(s).DNSNameASCII() == "*"
 			}
-			return api.Wildcard(s).DNSNameASCII() == "*."+s
+			return domain.Wildcard(s).DNSNameASCII() == "*."+s
 		},
 		nil,
 	))
@@ -65,7 +65,7 @@ func TestWildcardDescribe(t *testing.T) {
 		tc := tc
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
-			require.Equal(t, tc.expected, api.Wildcard(tc.input).Describe())
+			require.Equal(t, tc.expected, domain.Wildcard(tc.input).Describe())
 		})
 	}
 }
@@ -88,7 +88,7 @@ func TestWildcardSplitter(t *testing.T) {
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
 			var rs []r
-			for s := api.Wildcard(tc.input).Split(); s.IsValid(); s.Next() {
+			for s := domain.Wildcard(tc.input).Split(); s.IsValid(); s.Next() {
 				rs = append(rs, s.ZoneNameASCII())
 			}
 			require.Equal(t, tc.expected, rs)
diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go
index 0e24bbb..ae54cff 100644
--- a/internal/setter/setter_test.go
+++ b/internal/setter/setter_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -23,7 +24,7 @@ func TestSet(t *testing.T) {
 	type anys = []interface{}
 
 	const (
-		domain    = api.FQDN("sub.test.org")
+		domain    = domain.FQDN("sub.test.org")
 		ipNetwork = ipnet.IP6
 		record1   = "record1"
 		record2   = "record2"
@@ -276,6 +277,24 @@ func TestSet(t *testing.T) {
 				m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(nil, false)
 			},
 		},
+		"impossible-invalidIP-records": {
+			invalidIP,
+			true,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
+					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
+				)
+			},
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+				gomock.InOrder(
+					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1, record2: invalidIP}, true), //nolint:lll
+
+					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(true),
+					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(true),
+				)
+			},
+		},
 	} {
 		tc := tc
 		for _, proxied := range [...]bool{true, false} {
diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go
index 418c52d..87fcb11 100644
--- a/internal/updater/updater_test.go
+++ b/internal/updater/updater_test.go
@@ -8,8 +8,8 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 
-	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -18,9 +18,9 @@ import (
 
 //nolint:funlen,paralleltest,maintidx // updater.IPv6MessageDisplayed is a global variable
 func TestUpdateIPs(t *testing.T) {
-	domain4 := api.FQDN("ip4.hello")
-	domain6 := api.FQDN("ip6.hello")
-	domains := map[ipnet.Type][]api.Domain{
+	domain4 := domain.FQDN("ip4.hello")
+	domain6 := domain.FQDN("ip6.hello")
+	domains := map[ipnet.Type][]domain.Domain{
 		ipnet.IP4: {domain4},
 		ipnet.IP6: {domain6},
 	}
@@ -41,7 +41,7 @@ func TestUpdateIPs(t *testing.T) {
 	provider4 := func(ppfmt pp.PP, m *mocks.MockProvider) { m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP4).Return(ip4) }
 	provider6 := func(ppfmt pp.PP, m *mocks.MockProvider) { m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP6).Return(ip6) }
 
-	type mockproxied = map[api.Domain]bool
+	type mockproxied = map[domain.Domain]bool
 	proxiedNone := mockproxied{domain4: false, domain6: false}
 	proxiedBoth := mockproxied{domain4: true, domain6: true}
 
@@ -61,7 +61,7 @@ func TestUpdateIPs(t *testing.T) {
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
 			},
 		},
 		"ip4only/setfail": {
@@ -71,7 +71,7 @@ func TestUpdateIPs(t *testing.T) {
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false)
 			},
 		},
 		"ip6only": {
@@ -81,7 +81,7 @@ func TestUpdateIPs(t *testing.T) {
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true)
 			},
 		},
 		"ip6only/setfail": {
@@ -91,7 +91,7 @@ func TestUpdateIPs(t *testing.T) {
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(false)
 			},
 		},
 		"both": {
@@ -102,8 +102,8 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true),
 				)
 			},
 		},
@@ -115,8 +115,8 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true),
 				)
 			},
 		},
@@ -128,8 +128,8 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(false),
 				)
 			},
 		},
@@ -151,7 +151,7 @@ func TestUpdateIPs(t *testing.T) {
 				ipnet.IP6: provider6,
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true)
 			},
 		},
 		"ip6fails": {
@@ -174,7 +174,7 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
 			},
 		},
 		"ip6fails/again": {
@@ -194,7 +194,7 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(true)
 			},
 		},
 		"bothfail": {
@@ -239,7 +239,7 @@ func TestUpdateIPs(t *testing.T) {
 			},
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
 			},
 		},
 	} {
@@ -276,16 +276,16 @@ func TestUpdateIPs(t *testing.T) {
 
 //nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable
 func TestClearIPs(t *testing.T) {
-	domain4 := api.FQDN("ip4.hello")
-	domain6 := api.FQDN("ip6.hello")
-	domains := map[ipnet.Type][]api.Domain{
+	domain4 := domain.FQDN("ip4.hello")
+	domain6 := domain.FQDN("ip6.hello")
+	domains := map[ipnet.Type][]domain.Domain{
 		ipnet.IP4: {domain4},
 		ipnet.IP6: {domain6},
 	}
 
 	type mockproviders = map[ipnet.Type]bool
 
-	type mockproxied = map[api.Domain]bool
+	type mockproxied = map[domain.Domain]bool
 	proxiedNone := mockproxied{domain4: false, domain6: false}
 	proxiedBoth := mockproxied{domain4: true, domain6: true}
 
@@ -309,7 +309,7 @@ func TestClearIPs(t *testing.T) {
 			nil,
 			mockproviders{ipnet.IP4: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, netip.Addr{}, proxied).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true)
 			},
 		},
 		"ip4only/setfail": {
@@ -318,7 +318,7 @@ func TestClearIPs(t *testing.T) {
 			nil,
 			mockproviders{ipnet.IP4: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, netip.Addr{}, proxied).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(false)
 			},
 		},
 		"ip6only": {
@@ -327,7 +327,7 @@ func TestClearIPs(t *testing.T) {
 			nil,
 			mockproviders{ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, netip.Addr{}, proxied).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true)
 			},
 		},
 		"ip6only/setfail": {
@@ -336,7 +336,7 @@ func TestClearIPs(t *testing.T) {
 			nil,
 			mockproviders{ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, netip.Addr{}, proxied).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(false)
 			},
 		},
 		"both": {
@@ -346,8 +346,8 @@ func TestClearIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, netip.Addr{}, proxied).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true),
 				)
 			},
 		},
@@ -358,8 +358,8 @@ func TestClearIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, netip.Addr{}, proxied).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true),
 				)
 			},
 		},
@@ -370,8 +370,8 @@ func TestClearIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
 			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip4.hello"), ipnet.IP4, netip.Addr{}, proxied).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, api.FQDN("ip6.hello"), ipnet.IP6, netip.Addr{}, proxied).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(false),
 				)
 			},
 		},