Skip to main content

Best Practices

Master the art of building secure, performant, and maintainable applications with CocoBase.

Security Best Practices

API Key Management

Never Hard-Code API Keys:
// Bad - hardcoded credentials
final config = CocobaseConfig(
  apiKey: "hardcoded-key",  // DON'T DO THIS
);

// Good - use environment variables
const apiKey = String.fromEnvironment('COCOBASE_API_KEY');
final config = CocobaseConfig(apiKey: apiKey);

// Good - load from secure storage
final prefs = await SharedPreferences.getInstance();
final apiKey = prefs.getString('cocobase_api_key');
final config = CocobaseConfig(apiKey: apiKey!);

Rotate API Keys Regularly:
  • Generate new API keys every 90 days
  • Revoke old keys after migration
  • Use different keys for development, staging, and production
  • Never commit API keys to version control
Add to .gitignore:
# .gitignore
.env
.env.local
config/secrets.json
**/secrets/

Authentication Security

Always Verify User Authentication:
def main():
    # Check if user is authenticated
    user = req.user

    if not user:
        return {"error": "Authentication required"}, 401

    # Proceed with authenticated operations
    return {"user_id": user.id}

Implement Role-Based Access Control:
def main():
    user = req.user

    if not user:
        return {"error": "Authentication required"}, 401

    # Check user roles
    if 'admin' not in user.roles:
        return {"error": "Forbidden - admin access required"}, 403

    # Perform admin operation
    result = perform_admin_action()
    return {"result": result}

Input Validation

Always Validate User Input:
def main():
    email = req.get('email', '').strip()
    password = req.get('password', '')

    # Validate email
    if not email or '@' not in email:
        return {"error": "Invalid email format"}, 400

    # Validate password strength
    if len(password) < 8:
        return {"error": "Password must be at least 8 characters"}, 400

    # Proceed with registration
    user = db.create_app_user(email, password)
    return {"user": user}

Sanitize User Input:
function sanitizeInput(input) {
  // Remove HTML tags
  return input.replace(/<[^>]*>/g, "");
}

function createPost(title, content) {
  const sanitizedTitle = sanitizeInput(title.trim());
  const sanitizedContent = sanitizeInput(content.trim());

  return db.createDocument("posts", {
    title: sanitizedTitle,
    content: sanitizedContent,
  });
}

Data Modeling Best Practices

Schema Design

Use Clear, Descriptive Field Names:
// Good - clear intent
{
  "author_id": "user-123",        // Obviously a user reference
  "product_ids": ["p1", "p2"],    // Obviously multiple products
  "is_published": true,           // Boolean clarity
}

// Bad - ambiguous names
{
  "related": "...",      // Related to what?
  "ids": ["..."],        // IDs of what?
  "flag": true,          // Flag for what?
}
Normalize Data Appropriately:
// Good - normalized structure
const user = {
  id: "user-123",
  name: "John Doe",
  company_id: "company-456", // Reference to company
};

const company = {
  id: "company-456",
  name: "TechCorp",
  address: "123 Main St",
};

// Bad - denormalized (duplicated data)
const user = {
  id: "user-123",
  name: "John Doe",
  company_name: "TechCorp", // Duplicated
  company_address: "123 Main St", // Duplicated
};
Use Appropriate Data Types:
# Good - appropriate types
document = {
    "price": 29.99,           # Number, not string
    "quantity": 5,            # Integer
    "is_available": True,     # Boolean, not string
    "tags": ["tech", "new"],  # Array
    "created_at": "2024-01-15T10:30:00Z"  # ISO 8601 timestamp
}

# Bad - wrong types
document = {
    "price": "29.99",         # String instead of number
    "quantity": "5",          # String instead of int
    "is_available": "true",   # String instead of boolean
    "tags": "tech,new",       # String instead of array
    "created_at": "01/15/2024"  # Non-standard date format
}

Relationship Management

Follow Naming Conventions:
// Single references use id suffix
{
  "author_id": "user-123",
  "category_id": "cat-456",
  "company_id": "comp-789"
}

// Multiple references use _ids suffix
{
  "tag_ids": ["tag-1", "tag-2"],
  "follower_ids": ["user-4", "user-5"],
  "product_ids": ["prod-1", "prod-2", "prod-3"]
}
Cache Relationship Counts:
# Store counts for quick access
user_data = {
    "followers_count": 1500,  # Update when changed
    "following_count": 300,
    "posts_count": 42
}

# Instead of counting every time
# followers = db.get_user_relationships(user_id, "followers")
# count = followers["total"]  # Expensive!

Efficient Queries

Use Specific Queries:
// Good - specific query
final query = QueryBuilder()
  .where('status', 'published')
  .where('author_id', userId)
  .limit(50);

final docs = await db.listDocuments("posts", queryBuilder: query);

// Bad - fetching everything then filtering
final all = await db.listDocuments("posts");
final filtered = all.where((doc) =>
  doc.data['status'] == 'published' &&
  doc.data['author_id'] == userId
).toList();
Always Set Limits:
# Good - limited results
posts = db.list_documents("posts",
    status="published",
    limit=20  # Prevent loading too much
)

# Bad - no limit (could return millions)
posts = db.list_documents("posts",
    status="published"
)
Use Pagination:
// Good - paginate results
async function getAllUsers() {
  const users = [];
  let page = 1;
  const perPage = 100;
  let hasMore = true;

  while (hasMore) {
    const result = await db.listDocuments("users", {
      limit: perPage,
      offset: (page - 1) * perPage,
    });

    users.push(...result);
    hasMore = result.length === perPage;
    page++;
  }

  return users;
}

// Bad - fetch everything at once
const users = await db.listDocuments("users");
Select Only Needed Fields:
// Good - select specific fields
final docs = await db.listDocuments(
  "books",
  queryBuilder: QueryBuilder()
    .select('id')
    .select('title')
    .select('price')
    .limit(100),
);

// Bad - fetch all fields
final docs = await db.listDocuments("books");
Use Populate Wisely:
# Good - selective population
posts = db.query("posts",
    populate=["author"],  # Only what's needed
    limit=20
)

# Bad - populate everything
posts = db.query("posts",
    populate=["author", "category", "tags", "comments", "likes"],  # Too much
    limit=20
)

Error Handling Patterns

Comprehensive Error Handling

Future<void> performOperation() async {
  try {
    final result = await db.createDocument("posts", data);
    print('Success: ${result.id}');
  } on DioException catch (e) {
    if (e.response?.statusCode == 400) {
      print('Bad request - check your data');
    } else if (e.response?.statusCode == 401) {
      print('Unauthorized - login required');
      await handleSessionExpired();
    } else if (e.response?.statusCode == 403) {
      print('Forbidden - insufficient permissions');
    } else if (e.response?.statusCode == 404) {
      print('Not found');
    } else if (e.response?.statusCode == 429) {
      print('Rate limited - slow down');
      await Future.delayed(Duration(seconds: 5));
    } else {
      print('Error: ${e.message}');
    }
  } catch (e) {
    print('Unexpected error: $e');
  }
}

Retry Logic

Implement Exponential Backoff:
Future<T> withRetry<T>(
  Future<T> Function() operation, {
  int maxAttempts = 3,
}) async {
  for (int i = 0; i < maxAttempts; i++) {
    try {
      return await operation();
    } catch (e) {
      if (i == maxAttempts - 1) rethrow;

      // Exponential backoff: 2^i seconds
      await Future.delayed(Duration(seconds: 1 << i));
    }
  }
  throw Exception('Max retries exceeded');
}

// Use it
final books = await withRetry(() => db.listDocuments<Book>("books"));

Rate Limiting

Respect API Limits

Implement Client-Side Rate Limiting:
class RateLimiter {
  constructor(maxRequests = 100, windowMs = 60000) {
    this.requests = [];
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
  }

  async acquire() {
    const now = Date.now();

    // Remove old requests outside the window
    this.requests = this.requests.filter((time) => now - time < this.windowMs);

    // Check if we're at the limit
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = this.requests[0];
      const waitTime = this.windowMs - (now - oldestRequest);
      await sleep(waitTime);
      return this.acquire(); // Retry
    }

    this.requests.push(now);
  }
}

// Use it
const limiter = new RateLimiter(100, 60000); // 100 requests per minute

async function makeRequest() {
  await limiter.acquire();
  return db.listDocuments("posts");
}
Batch Requests When Possible:
# Good - batch operations
def import_books(books_data):
    batch_size = 100
    for i in range(0, len(books_data), batch_size):
        batch = books_data[i:i + batch_size]
        db.bulk_create_documents("books", batch)

# Bad - individual requests
def import_books(books_data):
    for book in books_data:
        db.create_document("books", book)  # Too many requests!

Production Checklist

Before deploying to production, verify:

Security

  • API keys are stored securely (environment variables, not hardcoded)
  • HTTPS is enabled for all API calls
  • User authentication is properly validated
  • Input validation is implemented
  • Sensitive data is encrypted
  • Rate limiting is configured
  • Error messages don’t leak sensitive information

Performance

  • Indexes are created on frequently queried fields
  • Pagination is implemented for large datasets
  • Query limits are set appropriately
  • Caching is implemented where appropriate
  • Only necessary fields are selected/populated
  • Batch operations are used for bulk actions

Code Quality

  • Error handling is comprehensive
  • Logging is implemented
  • Code follows naming conventions
  • Tests are written and passing
  • Documentation is up to date
  • Type safety is enforced (where applicable)

Data Management

  • Database schema is properly designed
  • Relationships are clearly defined
  • Backup strategy is in place
  • Data migration plan exists
  • Soft deletes are used where appropriate

Monitoring

  • Error tracking is set up (e.g., Sentry)
  • Performance monitoring is enabled
  • Logs are centralized and searchable
  • Alerts are configured for critical issues

Performance Tips

1. Use Connection Pooling

// 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,
})

2. Minimize Network Requests

// Bad - N+1 query problem
final posts = await db.listDocuments("posts", limit: 10);
for (var post in posts) {
  final author = await db.getDocument("users", post.data['author_id']);
  print('Author: ${author.data['name']}');
}

// Good - use populate
final posts = await db.listDocuments(
  "posts",
  queryBuilder: QueryBuilder().populate('author'),
  limit: 10
);
for (var post in posts) {
  print('Author: ${post.data['author']['name']}');
}

3. Use Bulk Operations

// Bad - individual creates
for (const book of books) {
  await db.createDocument("books", book);
}

// Good - batch create
await db.batchCreateDocuments("books", books);

4. Implement Lazy Loading

Load data only when needed to improve initial load time.

5. Optimize Images and Files

  • Compress images before upload
  • Use appropriate formats (WebP for web)
  • Implement lazy loading for images
  • Use CDN for static assets

Testing Best Practices

Unit Testing

// Flutter example
void main() {
  group('Book operations', () {
    late Cocobase db;

    setUp(() {
      final config = CocobaseConfig(apiKey: 'test-key');
      db = Cocobase(config);
    });

    test('should create a book', () async {
      final book = await db.createDocument('books', {
        'title': 'Test Book',
        'author': 'Test Author',
        'price': 19.99,
      });

      expect(book.data['title'], 'Test Book');
      expect(book.data['price'], 19.99);
    });

    test('should handle validation errors', () async {
      expect(
        () => db.createDocument('books', {}),
        throwsA(isA<ValidationError>()),
      );
    });
  });
}

Next Steps