The official Go SDK for HelixDB
- Prerequisites
- Installation
- Quick Start
- Client Configuration
- Making Queries
- Handling Responses
- Complete Example
- Best Practices
Before using this SDK, ensure you have:
- HelixDB running: The database should be accessible at your specified host
- HelixQL schema and queries defined: Your database schema and query endpoints should be deployed
For HelixDB setup, visit the official documentation.
go get github.com/HelixDB/helix-gopackage main
import (
"time"
"github.com/HelixDB/helix-go"
)
func main() {
// Create client with default timeout (10 seconds)
client := helix.NewClient("http://localhost:6969")
// Or with custom timeout
client = helix.NewClient(
"http://localhost:6969",
helix.WithTimeout(30*time.Second),
)
}All queries follow this simple pattern:
res, err := client.Query("<endpoint>", /* optional options... */)
if err != nil {
// handle error
}
// Choose how to handle the response:
err = res.Scan(&destStruct) // structured
m, err := res.AsMap() // dynamic map
raw := res.Raw() // raw bytesWhere:
<endpoint>is your HelixQL query name- The response handling is done via methods on
*Response:Scan(...)for typed decodingAsMap()for dynamic accessRaw()for raw bytes
Configure how long the client waits for responses:
client := helix.NewClient(
"http://localhost:6969",
helix.WithTimeout(5*time.Second),
)// schema.hx
N::User {
name: String,
age: U32,
email: String,
created_at: I32,
}
E::Follows {
From: User,
To: User,
Properties: {
since: I32,
}
}// queries.hx
QUERY create_user(name: String, age: U32, email: String, now: I32) =>
user <- AddN<User>({name: name, age: age, email: email, created_at: now})
RETURN user
QUERY get_users() =>
users <- N<User>
RETURN users
QUERY follow(follower_id: ID, followed_id: ID) =>
follower <- N<User>(follower_id)
followed <- N<User>(followed_id)
AddE<Follows>::From(follower)::To(followed)
RETURN "Success"
QUERY followers(id: ID) =>
followers <- N<User>(id)::In<Follows>
RETURN followers
QUERY following(id: ID) =>
following <- N<User>(id)::Out<Follows>
RETURN following The WithData option lets you pass input data to your queries. It accepts multiple data types.
userData := map[string]any{
"name": "John",
"age": 25,
}
res, err := client.Query("create_user", helix.WithData(userData))
// handle err and use res...type UserInput struct {
Name string `json:"name"`
Age int `json:"age"`
}
input := UserInput{Name: "John", Age: 25}
res, err := client.Query("create_user", helix.WithData(input))
// handle err and use res...jsonData := `{"name": "John", "age": 25}`
res, err := client.Query("create_user", helix.WithData(jsonData))
// handle err and use res...jsonBytes := []byte(`{"name": "John", "age": 25}`)
res, err := client.Query("create_user", helix.WithData(jsonBytes))
// handle err and use res...Choose the response method that best fits your needs:
The most powerful method for handling structured responses.
type CreateUserResponse struct {
User User `json:"user"`
}
res, err := client.Query("create_user", helix.WithData(userData))
if err != nil {
log.Fatal(err)
}
var response CreateUserResponse
if err := res.Scan(&response); err != nil {
log.Fatal(err)
}
// Access: response.UserExtract only the fields you need from the response:
// Single field extraction
res, err := client.Query("get_users")
if err != nil {
log.Fatal(err)
}
var users []User
if err := res.Scan(helix.WithDest("users", &users)); err != nil {
log.Fatal(err)
}
// Multiple field extraction
res, err = client.Query("get_users_with_count")
if err != nil {
log.Fatal(err)
}
var totalCount int
if err := res.Scan(
helix.WithDest("users", &users),
helix.WithDest("total_count", &totalCount),
); err != nil {
log.Fatal(err)
}When to use WithDest:
- You only need specific fields from a large response
- The response contains multiple top-level fields
- You want to avoid creating response wrapper structs
Get the response as a Go map for flexible access:
res, err := client.Query("get_users")
if err != nil {
log.Fatal(err)
}
responseMap, err := res.AsMap()
if err != nil {
log.Fatal(err)
}
// Access nested data
users := responseMap["users"]
fmt.Println(users)
// Type assertion for further processing
if usersList, ok := responseMap["users"].([]interface{}); ok {
fmt.Printf("Found %d users\n", len(usersList))
}When to use AsMap:
- Response structure is unknown or varies
- For debugging and exploration
- When you need flexible access to response data
Get the raw byte response from HelixDB:
res, err := client.Query("get_users")
if err != nil {
log.Fatal(err)
}
rawBytes := res.Raw()
// Process raw JSON
fmt.Println(string(rawBytes))
// Manual unmarshaling
var customResult MyCustomStruct
if err := json.Unmarshal(rawBytes, &customResult); err != nil {
log.Fatal(err)
}When to use Raw:
- You need maximum control over response processing
- For custom JSON unmarshaling logic
Here's a comprehensive example demonstrating user management and relationships:
// main.go
package main
import (
"fmt"
"github.com/HelixDB/helix-go"
"log"
"time"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Age int32 `json:"age"`
Email string `json:"email"`
CreatedAt int32 `json:"created_at"`
}
type CreateUserResponse struct {
User User `json:"user"`
}
type FollowUserInput struct {
FollowerId string `json:"follower_id"`
FollowedId string `json:"followed_id"`
}
func main() {
// Initialize client
client := helix.NewClient("http://localhost:6969")
now := int32(time.Now().Unix())
// Create first user
userData1 := map[string]any{
"name": "Alice Johnson",
"age": 28,
"email": "[email protected]",
"now": now,
}
res, err := client.Query("create_user", helix.WithData(userData1))
if err != nil {
log.Fatal(err)
}
var createResponse1 CreateUserResponse
if err := res.Scan(&createResponse1); err != nil {
log.Fatal(err)
}
fmt.Printf("\nCreated user 1: %+v\n", createResponse1.User)
// Create second user
userData2 := map[string]any{
"name": "Bob Smith",
"age": 32,
"email": "[email protected]",
"now": now,
}
res, err = client.Query("create_user", helix.WithData(userData2))
if err != nil {
log.Fatal(err)
}
var createResponse2 CreateUserResponse
if err := res.Scan(&createResponse2); err != nil {
log.Fatal(err)
}
fmt.Printf("\nCreated user 2: %+v\n", createResponse2.User)
// Get all users using WithDest
res, err = client.Query("get_users")
if err != nil {
log.Fatal(err)
}
var users []User
if err := res.Scan(helix.WithDest("users", &users)); err != nil {
log.Fatal(err)
}
fmt.Printf("\nTotal users: %d\n", len(users))
// Create follow relationship: Alice follows Bob
followData := &FollowUserInput{
FollowerId: createResponse1.User.ID,
FollowedId: createResponse2.User.ID,
}
res, err = client.Query("follow", helix.WithData(followData))
if err != nil {
log.Fatal(err)
}
// Use Raw() for operations that don't return structured data
_ = res.Raw()
fmt.Printf("\n%s now follows %s\n", createResponse1.User.Name, createResponse2.User.Name)
// Get Bob's followers using WithDest
res, err = client.Query("followers",
helix.WithData(map[string]any{"id": createResponse2.User.ID}),
)
if err != nil {
log.Fatal(err)
}
var followers []User
if err := res.Scan(helix.WithDest("followers", &followers)); err != nil {
log.Fatal(err)
}
fmt.Printf("\n%s has %d followers:\n", createResponse2.User.Name, len(followers))
for _, follower := range followers {
fmt.Printf("\t%s\n", follower.Name)
}
// Get Alice's following using AsMap for demonstration
res, err = client.Query("following",
helix.WithData(map[string]any{"id": createResponse1.User.ID}),
)
if err != nil {
log.Fatal(err)
}
followingMap, err := res.AsMap()
if err != nil {
log.Fatal(err)
}
if followingList, ok := followingMap["following"].([]any); ok {
fmt.Printf("\n%s is following %d users\n", createResponse1.User.Name, len(followingList))
for _, userFollowing := range followingList {
if m, ok := userFollowing.(map[string]any); ok {
fmt.Printf("\t%v\n", m["name"])
}
}
}
fmt.Println("Example completed successfully!")
}This example demonstrates:
- Client initialization with default settings
- Creating multiple users with
WithDataandScan - Querying data with field-specific extraction using
WithDest - Creating relationships between users using
Raw()for operations - Fetching related data (followers/following) with different response methods
- Using AsMap for flexible response handling
- Use
Scan()when you know the response structure and want type safety - Use
Scan()withWithDest()when you only need specific fields from large responses - Use
AsMap()for exploration, debugging, or when response structure varies - Use
Raw()when you need custom processing or maximum control
- Prefer structs for type safety and clearer code
- Use maps for flexible input scenarios
- Avoid slices/arrays as top-level input — HelixDB expects key–value objects
- Use JSON strings/bytes only when you're manually preparing JSON
If you encounter issues or want to contribute, feel free to open an issue or submit a PR on the GitHub repository.
Happy querying with HelixDB 🚀