Best Practices
Guidelines and recommendations for using the Cocobase Go SDK effectively.
Table of Contents
- General Principles
- Client Configuration
- Error Handling
- Query Optimization
- Authentication
- Data Management
- Real-time Updates
- Performance
- Security
- Testing
General Principles
Use Context Everywhere
Always use context.Context for cancellation and timeouts:
// Good: Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
doc, err := client.GetDocument(ctx, "users", id)
// Bad: No context control
doc, err := client.GetDocument(context.Background(), "users", id)
Check All Errors
Never ignore errors:
// Good: Check errors
doc, err := client.GetDocument(ctx, "users", id)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
// Bad: Ignore errors
doc, _ := client.GetDocument(ctx, "users", id)
Use Defer for Cleanup
Always clean up resources:
// Good: Defer cleanup
conn, err := client.WatchCollection(ctx, "users", handler, "watcher")
if err != nil {
return err
}
defer conn.Close()
// Bad: No cleanup guarantee
conn, err := client.WatchCollection(ctx, "users", handler, "watcher")
// Might forget to close
Client Configuration
Single Client Instance
Create one client instance and reuse it:
// Good: Single instance
var client *cocobase.Client
func init() {
client = cocobase.NewClient(cocobase.Config{
APIKey: os.Getenv("COCOBASE_API_KEY"),
})
}
// Bad: Creating clients repeatedly
func getUser() {
client := cocobase.NewClient(...) // Don't do this
}
Use Environment Variables
Store configuration in environment variables:
// Good: Environment variables
client := cocobase.NewClient(cocobase.Config{
APIKey: os.Getenv("COCOBASE_API_KEY"),
BaseURL: os.Getenv("COCOBASE_BASE_URL"),
})
// Bad: Hard-coded credentials
client := cocobase.NewClient(cocobase.Config{
APIKey: "hardcoded-api-key", // Don't do this
})
Enable Storage in Production
Use persistent storage for better UX:
// Good: Persistent storage
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}
client := cocobase.NewClient(cocobase.Config{
APIKey: apiKey,
Storage: store,
})
// Try to restore session
client.InitAuth(ctx)
Error Handling
Handle Specific Status Codes
// Good: Specific handling
doc, err := client.GetDocument(ctx, "users", id)
if err != nil {
if apiErr, ok := err.(*cocobase.APIError); ok {
switch apiErr.StatusCode {
case 404:
return ErrUserNotFound
case 403:
return ErrPermissionDenied
default:
return fmt.Errorf("unexpected error: %w", err)
}
}
return err
}
// Bad: Generic handling
if err != nil {
log.Fatal(err) // Too harsh for recoverable errors
}
Wrap Errors with Context
// Good: Contextual errors
doc, err := client.GetDocument(ctx, "users", userID)
if err != nil {
return fmt.Errorf("failed to get user %s: %w", userID, err)
}
// Bad: Raw errors
if err != nil {
return err // Lost context
}
Implement Retry Logic
// Good: Retry transient errors
func getDocumentWithRetry(client *cocobase.Client, ctx context.Context,
collection, id string) (*cocobase.Document, error) {
maxRetries := 3
for i := 0; i < maxRetries; i++ {
doc, err := client.GetDocument(ctx, collection, id)
if err == nil {
return doc, nil
}
if apiErr, ok := err.(*cocobase.APIError); ok {
if apiErr.StatusCode == 429 || apiErr.StatusCode >= 500 {
time.Sleep(time.Duration(i+1) * time.Second)
continue
}
}
return nil, err
}
return nil, fmt.Errorf("max retries exceeded")
}
Query Optimization
Use Specific Queries
// Good: Specific query
query := cocobase.NewQuery().
Where("status", "active").
Where("type", "premium").
Limit(50)
docs, err := client.ListDocuments(ctx, "users", query)
// Bad: Fetching everything
docs, err := client.ListDocuments(ctx, "users", nil)
Always Set Limits
// Good: Limited results
query := cocobase.NewQuery().
Where("status", "active").
Limit(100) // Prevent loading too much
// Bad: No limit
query := cocobase.NewQuery().
Where("status", "active") // Could return millions
Use Pagination for Large Datasets
// Good: Paginate results
func getAllUsers(client *cocobase.Client, ctx context.Context) ([]cocobase.Document, error) {
var allDocs []cocobase.Document
page := 1
perPage := 100
for {
query := cocobase.NewQuery().
Page(page, perPage).
Active()
docs, err := client.ListDocuments(ctx, "users", query)
if err != nil {
return nil, err
}
allDocs = append(allDocs, docs...)
if len(docs) < perPage {
break
}
page++
}
return allDocs, nil
}
Order for Consistency
// Good: Stable ordering
query := cocobase.NewQuery().
Where("status", "active").
OrderBy("id"). // Stable sort
Limit(50)
// Bad: No ordering (results may vary)
query := cocobase.NewQuery().
Where("status", "active").
Limit(50)
Use Helper Methods
// Good: Use helpers
query := cocobase.NewQuery().
Active().
Recent().
Limit(50)
// Less readable
query := cocobase.NewQuery().
IsNull("deletedAt").
OrderByDesc("created_at").
Limit(50)
Authentication
Initialize Auth on Startup
// Good: Try to restore session
func initClient() (*cocobase.Client, error) {
store, _ := storage.NewFileStorage(".cocobase/auth.json")
client := cocobase.NewClient(cocobase.Config{
APIKey: os.Getenv("COCOBASE_API_KEY"),
Storage: store,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := client.InitAuth(ctx)
if err != nil {
// No existing session, that's okay
log.Println("No existing session")
}
return client, nil
}
Check Authentication Before Protected Operations
// Good: Check first
func deleteUser(client *cocobase.Client, ctx context.Context, id string) error {
if !client.IsAuthenticated() {
return ErrNotAuthenticated
}
if !client.HasRole("admin") {
return ErrPermissionDenied
}
return client.DeleteDocument(ctx, "users", id)
}
Logout on Exit
// Good: Clean logout
func main() {
client := initClient()
defer client.Logout()
// Application logic
}
Data Management
Validate Data Before Sending
// Good: Validate first
func createUser(client *cocobase.Client, name, email string, age int) error {
if name == "" {
return fmt.Errorf("name is required")
}
if !isValidEmail(email) {
return fmt.Errorf("invalid email format")
}
if age < 0 || age > 150 {
return fmt.Errorf("invalid age")
}
data := map[string]interface{}{
"name": name,
"email": email,
"age": age,
}
_, err := client.CreateDocument(ctx, "users", data)
return err
}
Use Soft Deletes
// Good: Soft delete
func softDelete(client *cocobase.Client, ctx context.Context, collection, id string) error {
updates := map[string]interface{}{
"deletedAt": time.Now().Format(time.RFC3339),
}
_, err := client.UpdateDocument(ctx, collection, id, updates)
return err
}
// Query non-deleted documents
query := cocobase.NewQuery().Active()
Include Timestamps
// Good: Add timestamps
data := map[string]interface{}{
"name": "Alice",
"email": "alice@example.com",
"createdAt": time.Now().Format(time.RFC3339),
}
doc, err := client.CreateDocument(ctx, "users", data)
// On update
updates := map[string]interface{}{
"status": "active",
"updatedAt": time.Now().Format(time.RFC3339),
}
Type-Safe Data Access
// Good: Safe type assertions
doc, err := client.GetDocument(ctx, "users", id)
if err != nil {
return err
}
name, ok := doc.Data["name"].(string)
if !ok {
return fmt.Errorf("invalid name field")
}
age, ok := doc.Data["age"].(float64)
if !ok {
return fmt.Errorf("invalid age field")
}
// Use name and age safely
Real-time Updates
Always Close Connections
// Good: Defer close
conn, err := client.WatchCollection(ctx, "users", handler, "watcher")
if err != nil {
return err
}
defer conn.Close()
Protect Event Handlers
// Good: Panic protection
handler := func(event cocobase.Event) {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in event handler: %v\n", r)
}
}()
processEvent(event)
}
Keep Handlers Fast
// Good: Offload heavy work
handler := func(event cocobase.Event) {
// Quick processing
go heavyProcessing(event) // Offload to goroutine
}
// Bad: Heavy processing in handler
handler := func(event cocobase.Event) {
heavyProcessing(event) // Blocks next event
}
Implement Reconnection
// Good: Handle disconnections
func watchWithReconnect(client *cocobase.Client, ctx context.Context,
collection string, handler func(cocobase.Event)) {
for {
conn, err := client.WatchCollection(ctx, collection, handler, "")
if err != nil {
log.Printf("Connection failed: %v\n", err)
time.Sleep(5 * time.Second)
continue
}
// Monitor connection
for !conn.IsClosed() {
time.Sleep(1 * time.Second)
}
log.Println("Connection lost, reconnecting...")
time.Sleep(2 * time.Second)
}
}
Performance
Reuse HTTP Client
// Good: Custom HTTP client with connection pooling
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
client := cocobase.NewClient(cocobase.Config{
APIKey: apiKey,
HTTPClient: httpClient,
})
Batch Operations
// Good: Process in batches
func processBatch(client *cocobase.Client, ctx context.Context, ids []string) error {
batchSize := 50
for i := 0; i < len(ids); i += batchSize {
end := i + batchSize
if end > len(ids) {
end = len(ids)
}
batch := ids[i:end]
// Process batch
for _, id := range batch {
// Process each item
}
}
return nil
}
Use Appropriate Timeouts
// Good: Different timeouts for different operations
func quickOperation(client *cocobase.Client) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return client.GetDocument(ctx, "users", id)
}
func longOperation(client *cocobase.Client) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return client.ListDocuments(ctx, "users", complexQuery)
}
Security
Store Credentials Securely
// Good: Use environment variables or secret management
apiKey := os.Getenv("COCOBASE_API_KEY")
// Bad: Hard-coded credentials
apiKey := "hardcoded-key" // Don't do this
Validate User Input
// Good: Validate and sanitize
func createPost(client *cocobase.Client, title, content string) error {
// Validate
if len(title) > 200 {
return fmt.Errorf("title too long")
}
if len(content) > 10000 {
return fmt.Errorf("content too long")
}
// Sanitize (example)
title = strings.TrimSpace(title)
content = strings.TrimSpace(content)
data := map[string]interface{}{
"title": title,
"content": content,
}
_, err := client.CreateDocument(ctx, "posts", data)
return err
}
Use HTTPS in Production
// Good: HTTPS for production
client := cocobase.NewClient(cocobase.Config{
APIKey: apiKey,
BaseURL: "https://api.cocobase.io", // HTTPS
})
// Bad: HTTP in production
// BaseURL: "http://api.cocobase.io", // Insecure
Restrict File Permissions
// Good: Secure file storage
store, err := storage.NewFileStorage(".cocobase/auth.json")
if err != nil {
log.Fatal(err)
}
// Set restrictive permissions
os.Chmod(".cocobase/auth.json", 0600) // Owner read/write only
Testing
Use Mock Storage for Tests
// Good: Mock storage for testing
func TestUserOperations(t *testing.T) {
store := storage.NewMemoryStorage()
client := cocobase.NewClient(cocobase.Config{
APIKey: "test-api-key",
Storage: store,
})
// Run tests
}
Test Error Cases
// Good: Test error scenarios
func TestGetDocumentNotFound(t *testing.T) {
client := setupTestClient()
_, err := client.GetDocument(ctx, "users", "nonexistent-id")
if err == nil {
t.Fatal("expected error, got nil")
}
if apiErr, ok := err.(*cocobase.APIError); ok {
if apiErr.StatusCode != 404 {
t.Fatalf("expected 404, got %d", apiErr.StatusCode)
}
} else {
t.Fatal("expected APIError")
}
}
Use Table-Driven Tests
// Good: Table-driven tests
func TestQueryBuilder(t *testing.T) {
tests := []struct {
name string
query *cocobase.QueryBuilder
expected string
}{
{
name: "simple where",
query: cocobase.NewQuery().Where("status", "active"),
expected: "status=active",
},
{
name: "with limit",
query: cocobase.NewQuery().Where("status", "active").Limit(10),
expected: "status=active&limit=10",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.query.Build()
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
Previous: API Reference | Back to Home: Documentation