If you are using Directus as a headless CMS or data platform and your application is written in Go, you have probably noticed an interesting gap: Directus has a very capable JavaScript/TypeScript SDK, but the Go ecosystem around it is much smaller.
That was the motivation behind directus-client-go, a typed Go client for the Directus REST API.
The goal was not to build another thin HTTP wrapper. I wanted something that feels natural to use from Go while staying reasonably close to the API exposed by Directus.
The project
The library is available on GitHub:
https://github.com/chop-sticks/directus-client-go
It can be installed directly with:
go get github.com/chop-sticks/directus-client-go/directus
The project is MIT licensed and is distributed as a standard Go module.
Why another Directus client?
Directus exposes a fairly large REST API.
There are endpoints for items and files, but also users, roles, permissions, policies, flows, operations, dashboards, panels, presets, translations, relations, settings, extensions, collections, fields, authentication, schema management and more.
A client can therefore become either:
- a very small wrapper around
net/http, leaving most of the work to the application, or - a large SDK that tries to model the entire Directus API.
I chose the second direction.
The library currently mirrors a broad portion of the Directus API command surface, including items, files, folders, users, roles, policies, permissions, flows, operations, panels, dashboards, presets, translations, shares, comments, notifications, activity, revisions, content versions, relations, settings, extensions, collections and fields.
A simple example
Getting started is intentionally boring.
And boring is good for an API client.
package main
import (
"fmt"
"github.com/chop-sticks/directus-client-go/directus"
)
func main() {
host := "http://localhost:8055"
token := "your-static-token"
client, err := directus.NewClient(&host, &token)
if err != nil {
panic(err)
}
limit := 10
articles, err := client.GetItems("articles", &directus.Query{
Fields: []string{"id", "title", "author.name"},
Filter: map[string]any{
"status": map[string]any{
"_eq": "published",
},
},
Sort: []string{"-date_created"},
Limit: &limit,
})
if err != nil {
panic(err)
}
fmt.Println(articles)
}
The query structure follows Directus' own query model, including fields, filters, sorting, pagination, search, deep queries, aggregation, grouping, aliases and other query parameters.
Working with items
The client exposes the standard CRUD operations you'd expect from a Directus SDK.
For example:
created, err := client.CreateItem("articles", map[string]any{
"title": "Hello from Go",
"status": "draft",
}, nil)
if err != nil {
panic(err)
}
fmt.Println(created["id"])
For existing collections, the API follows consistent method conventions:
GetXs(q) // list
GetX(id, q) // get one
CreateX(...) // create one
CreateXs(...) // create many
PatchX(id, ...) // update one
PatchXs(keys, ...) // update by keys
PatchXsBatch(...) // batch update
DeleteX(id) // delete one
DeleteXs(keys) // delete many
This consistency becomes increasingly useful once you start working with more than one Directus resource.
Typed models where they make sense
One of the things I wanted to avoid was forcing everything through map[string]any.
Directus collections are dynamic by design, so arbitrary collections still need a flexible representation.
But Directus also has core system collections with well-defined schemas.
For those, the client provides typed models.
For example:
me, err := client.GetUsersMe(&directus.Query{
Fields: []string{"id", "email"},
})
if err != nil {
panic(err)
}
fmt.Println(me.Email)
This gives you the flexibility of Directus for user-defined collections while still getting normal Go structs for system resources.
Authentication
The simplest option is to initialize the client with a static bearer token:
client, err := directus.NewClient(&host, &token)
The client also supports logging in with Directus credentials and then using the returned access token:
auth, err := client.Login(
"admin@example.com",
"password",
"json",
"",
)
if err != nil {
panic(err)
}
client.Token = auth.AccessToken
This makes it possible to use the same client for applications that rely on a preconfigured token as well as applications that need to authenticate dynamically.
Testing against a real Directus instance
One part of the project that I consider particularly important is the integration test suite.
Mocking HTTP responses is useful, but it doesn't tell you whether the client actually works with a real Directus installation.
The repository therefore includes a Docker Compose based integration environment with Directus, PostgreSQL/PostGIS and Redis.
Integration tests are separated using Go build tags, so the normal test suite remains fast:
go test ./...
Integration tests can be run with:
task test:integration
Or manually:
docker compose up -d --wait
go test \
-tags=integration \
-count=1 \
./directus/integration/...
docker compose down
The integration environment can also be configured using DIRECTUS_URL and DIRECTUS_TOKEN.
This gives the project two useful layers of testing:
┌───────────────────────┐
│ Unit tests │
│ fast feedback │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Integration tests │
│ real Directus API │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Docker Compose stack │
│ Directus + DB + Redis │
└───────────────────────┘
Keeping the development workflow simple
The repository uses Task for common development commands.
For example:
task build
task test
task test:cover
task test:race
task lint
task fmt
task vet
task tidy
There is also a task ci command that runs the main quality checks together.
Documentation and API reference generation are part of the development workflow as well.
The intention is that contributing to the project should not require remembering a collection of custom commands.
What I want this library to become
The project is still evolving.
The Directus API is large, and keeping an SDK synchronized with it is an ongoing job rather than something you finish once and put on a shelf.
The direction I'm aiming for is:
- broad coverage of the Directus REST API
- predictable Go naming and method conventions
- typed models for system resources
- flexible support for arbitrary Directus collections
- Directus-compatible query parameters
- good unit-test coverage
- real integration tests against Directus
- documentation that makes extending the client straightforward
I also want the implementation to remain relatively boring internally.
There is a lot of value in an SDK where adding a new endpoint doesn't require learning an elaborate framework first.
If you use Directus and Go
If you're building a Go application around Directus, I'd love for you to give the client a try:
Repository:
https://github.com/chop-sticks/directus-client-go
Go package:
https://pkg.go.dev/github.com/chop-sticks/directus-client-go
The package is already published as a Go module, with v1 releases available through the Go module ecosystem.
Issues, feedback and pull requests are welcome.
If you're using Directus in a Go project and find a missing endpoint or an API shape that doesn't fit well with the client, that's particularly useful feedback. Those are exactly the kinds of things that help an SDK mature from "works for my project" into something other people can confidently depend on.
Top comments (0)