DEV Community

Jakub Spórna
Jakub Spórna

Posted on

Building a Directus API Client for Go

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. a very small wrapper around net/http, leaving most of the work to the application, or
  2. 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)
}
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ./...
Enter fullscreen mode Exit fullscreen mode

Integration tests can be run with:

task test:integration
Enter fullscreen mode Exit fullscreen mode

Or manually:

docker compose up -d --wait

go test \
  -tags=integration \
  -count=1 \
  ./directus/integration/...

docker compose down
Enter fullscreen mode Exit fullscreen mode

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 │
             └───────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)