go-unifi

Architecture

How go-unifi is built — generated resource code vs. the hand-written client layer, the Client interface, and the one-way unifi → official dependency.

This section is for people who work on go-unifi rather than with it. It explains how the library is put together so you can change it safely. If you just want to call the API, start with the guides instead.

The library has two halves: a large body of generated code describing every resource the controller exposes, and a thin hand-written layer that turns that code into a usable client.

Generated vs. hand-written

Most resource types and their CRUD methods are generated directly from the UniFi controller's own API definitions. This is what gives the library its broad, accurate coverage — there are far more resources than anyone would hand-maintain.

Files named *.generated.go begin with // Code generated … DO NOT EDIT. Never hand-edit them. CI regenerates and overwrites them, so your change would be silently lost. To change generated output, edit the codegen customizations or add a hand-written sibling file.

The split inside unifi/ looks like this:

File patternOriginContains
*.generated.gocode generationResource structs, private CRUD methods (getUser, listUser), the Client/InternalClient interfaces, the moq mock
<resource>.gohand-writtenPublic wrappers (GetUser, ListUser), business logic (search-by-MAC, field init, custom marshaling)
client.go, requests.go, interceptors.go, json.go, …hand-writtenTransport, config, auth, error handling, JSON edge cases

Generated CRUD methods are deliberately private. The public surface lives in the hand-written sibling, so a regeneration never clobbers your custom logic. See Contributing for the wrapper convention.

The Client interface

The generated Client interface embeds InternalClient — the interface carrying every resource CRUD method — and then adds transport/lifecycle methods (Get/Post/Put/Patch/ Delete/Do, BaseURL, Version) plus the two API-surface accessors:

surfaces.go
package developers

import (
	"context"

	"github.com/filipowm/go-unifi/v2/unifi"
	"github.com/filipowm/go-unifi/v2/unifi/official"
)

// exampleSurfaces shows the two API surfaces hanging off one client.
func exampleSurfaces(ctx context.Context, c unifi.Client) {
	// Internal() returns the legacy resource CRUD surface (the default).
	var internal unifi.InternalClient = c.Internal()

	// Official() returns the Official OpenAPI surface, bound to the same transport.
	var off official.Client = c.Official()

	_, _ = internal.ListNetwork(ctx, "default")
	_ = off
}

Because the resource methods are promoted through the embedded InternalClient, c.ListNetwork(...) and c.Internal().ListNetwork(...) are the same call. c.Internal() and c.Official() simply name the surface you mean. The full reference is on the client reference page and on pkg.go.dev.

The one-way unifi → official dependency

The Official OpenAPI surface lives in its own package, unifi/official. The dependency is strictly one-way:

unifi  ──imports──▶  unifi/official
   ▲                      │
   └──── imports nothing ─┘

unifi/official imports nothing from unifi. That keeps it independently testable and prevents an import cycle. The two packages meet at two small structural seams, both injected by unifi when it constructs the Official client:

The wiring lives in unifi/official_surface.go: Official() calls official.New(c, integrationV1Path, c.officialAvailable), passing the client as the Doer and its gate method as the Gate. Official wrappers pass full leading-/ paths under the integration/v1 prefix, which buildRequestURL resolves directly against the base URL — bypassing the legacy API-path logic. This is why unifi/official never needs to know about old-vs-new API path styles.

Where to go next

On this page