A typed Go client for the Teamwork Spaces REST API.
Module: github.com/teamwork/spacessdkgo
Go version: 1.24.2+
go get github.com/teamwork/spacessdkgopackage main
import (
"context"
"log/slog"
"net/url"
"os"
"github.com/teamwork/spacessdkgo/client"
"github.com/teamwork/spacessdkgo/models"
)
func main() {
c := client.NewClient(
"https://your-instance.teamwork.com",
client.WithAPIKey("your-api-key"),
client.WithLogLevel(slog.LevelInfo),
)
ctx := context.Background()
// List spaces
spaces, err := c.Spaces.List(ctx, url.Values{})
if err != nil {
slog.Error("failed to list spaces", "error", err)
os.Exit(1)
}
for _, space := range spaces.Spaces {
slog.Info("space", "id", space.ID, "title", space.Title)
}
}c := client.NewClient(baseURL string, opts ...client.Option) *client.Client| Option | Description |
|---|---|
WithAPIKey(key string) |
Sets Authorization: Bearer <key> on all requests |
WithHTTPClient(hc *http.Client) |
Replaces the default HTTP client |
WithLogLevel(level slog.Level) |
Sets the minimum log level |
WithLogger(logger *slog.Logger) |
Replaces the default structured logger |
WithMiddleware(mw MiddlewareFunc) |
Appends middleware to the chain |
The main.go demo reads:
SPACES_API_BASE_URL— API base URLSPACES_API_KEY— API key
Use util.LoadEnv() to load a .env file in your own tooling.
The client exposes one field per resource service:
c.Spaces // *client.SpaceService
c.Pages // *client.PageService
c.Comments // *client.CommentService
c.Tags // *client.TagService
c.Categories // *client.CategoryService
c.Search // *client.SearchService// Get a single space
space, err := c.Spaces.Get(ctx, id int64)
// List all spaces
spaces, err := c.Spaces.List(ctx, params url.Values)
// Create a space
space, err := c.Spaces.Create(ctx, &models.SpaceCreate{
Title: "Engineering Wiki",
Purpose: "Internal docs",
})
// Update a space (PATCH)
space, err := c.Spaces.Update(ctx, id int64, &models.SpaceUpdate{
Title: "Updated Title",
})
// Delete a space
err := c.Spaces.Delete(ctx, id int64)
// List collaborators
collaborators, err := c.Spaces.Collaborators(ctx, id int64)// Get a single page
page, err := c.Pages.Get(ctx, spaceID, pageID int64)
// List pages in a space
pages, err := c.Pages.List(ctx, spaceID int64, params url.Values)
// Get the home page of a space
home, err := c.Pages.Home(ctx, spaceID int64)
// Create a page
page, err := c.Pages.Create(ctx, spaceID int64, &models.PageCreate{
Title: "Getting Started",
Content: "<p>Welcome!</p>",
})
// Duplicate a page
page, err := c.Pages.Duplicate(ctx, spaceID, pageID int64, &models.PageDuplicate{
Title: "Copy of Getting Started",
})
// Update a page (PATCH)
page, err := c.Pages.Update(ctx, spaceID, pageID int64, &models.PageUpdate{
Content: "<p>Updated content</p>",
})
// Delete a page
err := c.Pages.Delete(ctx, spaceID, pageID int64)// Get a single comment
comment, err := c.Comments.Get(ctx, spaceID, pageID, commentID int64)
// List comments on a page
comments, err := c.Comments.List(ctx, spaceID, pageID int64, params url.Values)
// Create a comment
comment, err := c.Comments.Create(ctx, spaceID, pageID int64, &models.CommentCreate{
Content: "Great page!",
})
// Update a comment (PATCH)
comment, err := c.Comments.Update(ctx, spaceID, pageID, commentID int64, &models.CommentUpdate{
Content: "Edited: Great page!",
})
// Delete a comment
err := c.Comments.Delete(ctx, spaceID, pageID, commentID int64)// Get a single tag
tag, err := c.Tags.Get(ctx, id int64)
// List all tags
tags, err := c.Tags.List(ctx, params url.Values)
// Create multiple tags in one request
tags, err := c.Tags.CreateBatch(ctx, []models.Tag{
{Name: "go", Color: "#00ADD8"},
{Name: "api", Color: "#FF6B6B"},
})
// Update a tag (PATCH)
tag, err := c.Tags.Update(ctx, id int64, &models.TagUpdate{
Name: "golang",
})
// Delete a tag
err := c.Tags.Delete(ctx, id int64)// Get a single category
cat, err := c.Categories.Get(ctx, id int64)
// List all categories
cats, err := c.Categories.List(ctx, params url.Values)
// Create a category
cat, err := c.Categories.Create(ctx, &models.CategoryCreate{
Name: "Engineering",
Color: "#4A90E2",
})
// Update a category (PATCH)
cat, err := c.Categories.Update(ctx, id int64, &models.CategoryUpdate{
Name: "Engineering Teams",
})
// Delete a category
err := c.Categories.Delete(ctx, id int64)results, err := c.Search.Search(ctx, models.SearchFilter{
Query: "deployment guide",
SpaceID: []int64{42, 57},
Limit: func() *int64 { v := int64(20); return &v }(),
})
for _, r := range results.Results {
fmt.Printf("page %d: %s (space %d)\n", r.PageID, r.Title, r.Space.ID)
for field, snippet := range r.MatchedText {
fmt.Printf(" matched in %s: %s\n", field, snippet)
}
}Middleware wraps every HTTP request. Add it via WithMiddleware; multiple calls are allowed. The chain executes in reverse append order (last-added runs first).
c := client.NewClient(baseURL,
client.WithAPIKey(apiKey),
client.WithMiddleware(client.RetryMiddleware(3, 500*time.Millisecond)),
client.WithMiddleware(client.RequestIDMiddleware()),
client.WithMiddleware(client.LoggingMiddleware(logger)),
)| Middleware | Description |
|---|---|
LoggingMiddleware(logger) |
Logs method, URL, status, and duration |
RetryMiddleware(maxRetries, retryDelay) |
Retries failed requests |
AuthMiddleware(token) |
Sets Authorization: Bearer (alternative to WithAPIKey) |
UserAgentMiddleware(userAgent) |
Sets the User-Agent header |
RateLimitMiddleware(requestsPerSecond) |
Token-bucket rate limiting |
RequestIDMiddleware() |
Adds a unique X-Request-ID to each request |
TimeoutMiddleware(timeout) |
Wraps request context with a deadline |
HeaderMiddleware(headers) |
Adds static headers to all requests |
ConditionalMiddleware(condition, mw) |
Applies middleware only when condition returns true |
func TracingMiddleware(traceID string) client.MiddlewareFunc {
return func(ctx context.Context, req *http.Request, next client.RequestHandler) (*http.Response, error) {
req.Header.Set("X-Trace-ID", traceID)
return next(ctx, req)
}
}List responses include a Meta field with page information:
resp, err := c.Spaces.List(ctx, url.Values{
"page": []string{"2"},
"perPage": []string{"25"},
})
fmt.Println(resp.Meta.Page.Count) // total items
fmt.Println(resp.Meta.Page.Size) // page size
fmt.Println(resp.Meta.Page.Offset) // current offset| Model | Key Fields |
|---|---|
Space |
ID, Title, Code, Purpose, SpaceColor, Icon, State |
Page |
ID, Title, Slug, Content, ContentRevision, Tags, IsPublished, IsPrivate |
Comment |
ID, Content, ParentID, IsPrivate, State |
Tag |
ID, Name, Color, PageCount |
Category |
ID, Name, Color |
models.StateActive // "active"
models.StateArchived // "archived"
models.StateRemoved // "removed"
models.StateDeleted // "deleted"Get and List responses include an Included field with sideloaded related resources:
page, err := c.Pages.Get(ctx, spaceID, pageID)
// page.Included contains related entities embedded in the responseAll methods return (*T, error). Errors are plain error values — no custom types or sentinel errors.
space, err := c.Spaces.Get(ctx, 123)
if err != nil {
// err contains the HTTP status code and response body on API errors:
// "unexpected status code: 404, body: ..."
log.Fatal(err)
}The SDK ships a MockRoundTripper for unit tests. It intercepts HTTP calls without a real server.
import "github.com/teamwork/spacessdkgo/client"
func TestMyCode(t *testing.T) {
mock := client.NewMockRoundTripper()
mock.AddResponse(
http.MethodGet,
"/spaces/42.json",
http.StatusOK,
models.SpaceResponse{Space: models.Space{ID: 42, Title: "Docs"}},
)
c := client.NewClient("https://example.com",
client.WithHTTPClient(&http.Client{Transport: mock}),
)
space, err := c.Spaces.Get(context.Background(), 42)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if space.Space.Title != "Docs" {
t.Errorf("got %q, want %q", space.Space.Title, "Docs")
}
reqs := mock.GetRequests()
if len(reqs) != 1 {
t.Fatalf("expected 1 request, got %d", len(reqs))
}
}AddResponse accepts: io.ReadCloser, string, or any value (marshaled to JSON automatically).
.
├── api/ # Service interface definitions
├── client/ # HTTP client, all service implementations, middleware
│ ├── client.go # Client struct and constructor
│ ├── resource.go # Generic Service[T, L] base (Get/List/Create/Update)
│ ├── middleware.go # All middleware implementations
│ ├── transport.go # LoggingTransport (http.RoundTripper)
│ ├── mock_client.go # MockRoundTripper for tests
│ ├── filter.go # MongoDB-style FilterBuilder
│ ├── spaces.go
│ ├── pages.go
│ ├── comments.go
│ ├── tags.go
│ ├── categories.go
│ └── search.go
├── models/ # All data types: domain models and response wrappers
│ ├── base.go # BaseEntity, EntityRef, UserRef, State
│ ├── response.go # Pagination, Meta, IncludedData
│ └── <resource>.go # Domain model + request/response types per resource
├── util/
│ ├── env.go # .env loading helpers
│ └── json.go # MergeJSONData utility
└── main.go # Demo/CLI — not part of the library API
Follow the patterns in CLAUDE.md for all new code. Key rules:
- One file per resource in
client/andmodels/ - All HTTP calls go through
doRequest— never bypass it - Use
http.NewRequestWithContext— neverhttp.NewRequest - Return
(*T, error)— no panics in library code - Use
log/slog— neverfmt.Printlnorlog.Printf - Tests use only the standard
testingpackage andMockRoundTripper