Best Practices

Production-ready plugin development guidelines

Best Practices

Guidelines for building production-ready Orbis plugins.

Code Organization

SDK-Based Project Structure

text
my-plugin/
├── Cargo.toml
├── manifest.json
├── README.md
├── CHANGELOG.md
├── src/
│   ├── lib.rs          # Plugin entry + macros
│   ├── handlers.rs     # Route handlers
│   ├── models.rs       # Data structures
│   ├── services.rs     # Business logic
│   └── utils.rs        # Helpers
└── tests/
    ├── unit.rs
    └── integration.rs

Module Separation with SDK

rust
// src/lib.rs - Keep minimal, just SDK setup
use orbis_plugin_api::sdk::prelude::*;

mod handlers;
mod models;
mod services;

// Initialize plugin with zero boilerplate
orbis_plugin!();

// Export handlers
pub use handlers::*;
rust
// src/handlers.rs - Route handling
use crate::{models::*, services};
use orbis_plugin_api::sdk::prelude::*;
use serde_json::json;

fn get_items_impl(ctx: Context) -> Result<Response> {
    let filter = ctx.query_param("filter").unwrap_or("all");
    let items = services::get_filtered_items(filter)?;
    Response::json(&items)
}

fn create_item_impl(ctx: Context) -> Result<Response> {
    let new_item: NewItem = ctx.body_as()?;
    let item = services::create_item(new_item)?;
    Response::json(&json!({ "item": item }))
}

// Export handlers
wrap_handler!(get_items, get_items_impl);
wrap_handler!(create_item, create_item_impl);
rust
// src/services.rs - Business logic (testable!)
use crate::models::*;
use orbis_plugin_api::sdk::prelude::*;

pub fn get_filtered_items(filter: &str) -> Result<Vec<Item>> {
    let all_items: Vec<Item> = state::get("items").unwrap_or_default();
    
    let filtered = if filter == "all" {
        all_items
    } else {
        all_items.into_iter()
            .filter(|item| item.category == filter)
            .collect()
    };
    
    Ok(filtered)
}

pub fn create_item(new_item: NewItem) -> Result<Item> {
    let mut items: Vec<Item> = state::get("items").unwrap_or_default();
    
    let item = Item {
        id: items.len() as i32 + 1,
        name: new_item.name,
        category: new_item.category,
    };
    
    items.push(item.clone());
    state::set("items", &items)?;
    
    log::info!("Created item: {:?}", item);
    Ok(item)
}
rust
// src/models.rs - Data structures
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
    pub id: i32,
    pub name: String,
    pub category: String,
}

#[derive(Debug, Deserialize)]
pub struct NewItem {
    pub name: String,
    pub category: String,
}

SDK Best Practices

DO: Separate concerns

rust
// Good: Business logic separate from handlers
pub fn calculate_discount(price: f64, user_tier: &str) -> f64 {
    match user_tier {
        "premium" => price * 0.8,
        "gold" => price * 0.9,
        _ => price,
    }
}

fn get_price_impl(ctx: Context) -> Result<Response> {
    let tier = ctx.query_param("tier").unwrap_or("basic");
    let price = 100.0;
    let final_price = calculate_discount(price, tier);
    Response::json(&json!({ "price": final_price }))
}

wrap_handler!(get_price, get_price_impl);

❌ DON’T: Mix logic in handlers

rust
// Bad: Logic embedded in handler (hard to test)
fn get_price_impl(ctx: Context) -> Result<Response> {
    let tier = ctx.query_param("tier").unwrap_or("basic");
    let price = 100.0;
    let final_price = match tier {
        "premium" => price * 0.8,
        "gold" => price * 0.9,
        _ => price,
    };
    Response::json(&json!({ "price": final_price }))
}

wrap_handler!(get_price, get_price_impl);

DO: Use type-safe helpers

rust
// Good: Type-safe parsing
fn get_item_impl(ctx: Context) -> Result<Response> {
    let id: i32 = ctx.query_param_as("id")?;
    let items: Vec<Item> = state::get("items")?.unwrap_or_default();
    
    let item = items.iter()
        .find(|i| i.id == id)
        .ok_or_else(|| Error::NotFound)?;
    
    Response::json(item)
}

wrap_handler!(get_item, get_item_impl);

❌ DON’T: Manual parsing

rust
// Bad: Manual string parsing (error-prone)
fn get_item_impl(ctx: Context) -> Result<Response> {
    let id_str = ctx.query_param("id").unwrap_or("0");
    let id = id_str.parse::<i32>().unwrap(); // panics on invalid input!
    // ...
}

wrap_handler!(get_item, get_item_impl);

DO: Use logging

rust
// Good: Proper logging
fn delete_item_impl(ctx: Context) -> Result<Response> {
    let id: i32 = ctx.query_param_as("id")?;
    
    log::info!("Deleting item with id: {}", id);
    
    let mut items: Vec<Item> = state::get("items")?.unwrap_or_default();
    items.retain(|item| item.id != id);
    state::set("items", &items)?;
    
    log::info!("Item deleted successfully");
    Ok(Response::json(&json!({ "success": true })))
}

Schema Design

State Organization

json
{
  "state": {
    "data": {
      "type": "object",
      "default": {
        "items": [],
        "selectedItem": null
      }
    },
    "ui": {
      "type": "object", 
      "default": {
        "isLoading": false,
        "activeTab": "all",
        "searchQuery": ""
      }
    },
    "form": {
      "type": "object",
      "default": {
        "name": "",
        "description": ""
      }
    },
    "errors": {
      "type": "object",
      "default": {}
    }
  }
}

Naming Conventions

ElementConventionExample
Page IDskebab-caseitem-list
State pathscamelCaseselectedItem
Route pathskebab-case/my-plugin/items
Action typessnake_caseupdate_state
Component typesPascalCaseStatCard

Reusable Layouts

Define common patterns:

json
{
  "templates": {
    "pageWrapper": {
      "type": "Container",
      "className": "p-6 max-w-7xl mx-auto",
      "children": []
    },
    "cardGrid": {
      "type": "Grid",
      "columns": { "sm": 1, "md": 2, "lg": 3 },
      "gap": "1rem"
    }
  }
}

Performance

Minimize State Updates

json
{
  "events": {
    "on_click": [
      {
        "type": "update_state",
        "path": "ui.selectedId",
        "value": "{{$row.id}}"
      }
    ]
  }
}

❌ Avoid updating entire objects:

json
{
  "type": "update_state",
  "path": "data",
  "value": { "...all data plus changes..." }
}

Debounce Expensive Operations

json
{
  "fieldType": "text",
  "events": {
    "on_change": [
      {
        "type": "debounced_action",
        "delay": 300,
        "action": {
          "type": "call_api",
          "api": "search",
          "args": { "query": "{{state.searchQuery}}" }
        }
      }
    ]
  }
}

Lazy Loading

Load data only when needed:

json
{
  "type": "Tabs",
  "tabs": [
    { "id": "overview", "label": "Overview" },
    { "id": "details", "label": "Details" }
  ],
  "events": {
    "onTabChange": [
      {
        "type": "conditional",
        "condition": "{{$value}} === 'details' && !state.detailsLoaded",
        "then": [
          { "type": "call_api", "api": "getDetails" },
          { "type": "update_state", "path": "detailsLoaded", "value": true }
        ]
      }
    ]
  }
}

Optimize Lists

For large lists, use pagination:

json
{
  "type": "Table",
  "dataSource": "state:items",
  "pageSize": 25,
  "serverPagination": true,
  "events": {
    "on_page_change": [
      {
        "type": "call_api",
        "api": "getItems",
        "args": { 
          "page": "{{$page}}",
          "pageSize": 25
        }
      }
    ]
  }
}

Error Handling

Graceful Degradation

json
{
  "hooks": {
    "on_mount": [
      {
        "type": "call_api",
        "api": "getData",
        "on_success": [
          { "type": "update_state", "path": "data", "from": "$response.data" },
          { "type": "update_state", "path": "ui.loadError", "value": null }
        ],
        "on_error": [
          { "type": "update_state", "path": "ui.loadError", "from": "$error.message" }
        ]
      }
    ]
  }
}
json
{
  "type": "Conditional",
  "condition": "{{state.ui.loadError}}",
  "then": {
    "type": "Alert",
    "variant": "destructive",
    "title": "Failed to Load",
    "message": "{{state.ui.loadError}}",
    "action": {
      "type": "Button",
      "label": "Retry",
      "events": { "on_click": [{ "type": "call_api", "api": "getData" }] }
    }
  },
  "else": { "...normal content..." }
}

Validation Feedback

json
{
  "type": "Form",
  "fields": [
    {
      "name": "email",
      "fieldType": "text",
      "label": "Email",
      "validation": {
        "required": { "message": "Email is required" },
        "email": { "message": "Please enter a valid email" }
      }
    }
  ]
}

Error Boundaries

Your plugin is automatically wrapped in error boundaries. If a component crashes, only that section fails, not the entire app.

Security

Input Validation

Always validate in backend routes:

rust
fn create_item(body: &str) -> i32 {
    let input: CreateItemInput = match serde_json::from_str(body) {
        Ok(v) => v,
        Err(_) => return error_response("Invalid input"),
    };
    
    // Validate fields
    if input.name.trim().is_empty() {
        return error_response("Name is required");
    }
    
    if input.name.len() > 100 {
        return error_response("Name too long");
    }
    
    // Sanitize
    let name = sanitize_string(&input.name);
    
    // Process...
}

Permission Scoping

Request minimal permissions:

json
{
  "permissions": ["database:read"]
}

❌ Avoid over-permissioning:

json
{
  "permissions": ["database:read", "database:write", "filesystem", "network"]
}

Secure API Calls

json
{
  "type": "call_api",
  "api": "my-plugin.sensitiveAction",
  "args": {
    "id": "{{state.selectedId}}"
  }
}

Never expose secrets in state or expressions.

User Experience

Loading States

Always show loading feedback:

json
{
  "hooks": {
    "on_mount": [
      { "type": "set_loading", "loading": true },
      {
        "type": "call_api",
        "api": "getData",
        "on_success": [
          { "type": "update_state", "path": "data", "from": "$response.data" }
        ],
        "finally": [
          { "type": "set_loading", "loading": false }
        ]
      }
    ]
  }
}
json
{
  "type": "Conditional",
  "condition": "{{state.$loading}}",
  "then": { "type": "LoadingOverlay" }
}

Empty States

Handle empty data gracefully:

json
{
  "type": "Conditional",
  "condition": "{{state.items.length}} === 0",
  "then": {
    "type": "EmptyState",
    "icon": "FileText",
    "title": "No Items Yet",
    "description": "Create your first item to get started.",
    "action": {
      "type": "Button",
      "label": "Create Item",
      "events": { "on_click": [{ "type": "navigate", "to": "/items/new" }] }
    }
  },
  "else": { "type": "Table", "dataSource": "state:items" }
}

Feedback Actions

Confirm user actions:

json
{
  "events": {
    "on_click": [
      {
        "type": "call_api",
        "api": "saveItem",
        "on_success": [
          { "type": "show_toast", "message": "Saved successfully!", "level": "success" }
        ],
        "on_error": [
          { "type": "show_toast", "message": "Failed to save: {{$error.message}}", "level": "error" }
        ]
      }
    ]
  }
}

Confirm Destructive Actions

json
{
  "type": "Button",
  "label": "Delete",
  "variant": "destructive",
  "events": {
    "on_click": [
      {
        "type": "show_dialog",
        "title": "Delete Item?",
        "content": "This action cannot be undone.",
        "variant": "destructive",
        "confirmText": "Delete",
        "onConfirm": [
          { "type": "call_api", "api": "deleteItem" },
          { "type": "show_toast", "message": "Item deleted", "level": "success" },
          { "type": "navigate", "to": "/items" }
        ]
      }
    ]
  }
}

Accessibility

Semantic Structure

json
{
  "type": "Section",
  "ariaLabel": "User Statistics",
  "children": [
    { "type": "Heading", "level": 2, "text": "Statistics" },
    { "...content..." }
  ]
}

Form Labels

Always provide labels:

json
{
  "name": "email",
  "fieldType": "text",
  "label": "Email Address",
  "placeholder": "[email protected]",
  "required": true
}

Focus Management

json
{
  "type": "Modal",
  "autoFocus": true,
  "returnFocus": true,
  "events": {
    "on_close": [{ "type": "update_state", "path": "modalOpen", "value": false }]
  }
}

Keyboard Navigation

Ensure interactive elements are keyboard accessible:

json
{
  "type": "Button",
  "label": "Action",
  "ariaLabel": "Perform main action",
  "events": {
    "on_click": [{ "...actions..." }]
  }
}

Maintenance

Versioning

Follow semantic versioning:

json
{
  "version": "1.2.3"
}
  • Major (1.x.x): Breaking changes
  • Minor (x.1.x): New features
  • Patch (x.x.1): Bug fixes

Changelog

Keep a changelog:

markdown
# Changelog

## [1.2.0] - 2024-01-15
### Added
- New dashboard statistics cards
- Export functionality

### Fixed
- Form validation edge case

## [1.1.0] - 2024-01-01
### Added
- Initial release

Documentation

Document your plugin:

markdown
# My Plugin

## Features
- Feature 1
- Feature 2

## Installation
Copy `my_plugin.wasm` to plugins directory.

## Configuration
| Setting | Default | Description |
|---------|---------|-------------|
| `api_url` | `""` | Backend API URL |

## Usage
Navigate to `/my-plugin` to access the dashboard.

Checklist

Before Release

  • All tests passing
  • Manifest valid JSON
  • Version number updated
  • Changelog updated
  • README updated
  • Error states handled
  • Loading states visible
  • Empty states provided
  • Validation complete
  • WASM optimized (wasm-opt)
  • Debug code removed

Quality Gates

CheckThreshold
Test coverage> 80%
WASM size< 200KB
Load time< 100ms
No console errors0

Anti-Patterns

Avoid

Huge state objects - Split into logical groups

Inline styles - Use className with Tailwind

Polling for updates - Use subscriptions when available

Nested callbacks - Use action sequences

Hardcoded strings - Use state or config

Missing error handling - Always handle on_error

Prefer

  • Organized state - data, ui, form groups

  • Utility classes - Consistent styling

  • Debounced updates - For search/filtering

  • Action sequences - Clear flow control

  • Configurable values - Via plugin config

  • Graceful degradation - Show errors, allow retry

Next Steps