Best Practices
Production-ready plugin development guidelines
Best Practices
Guidelines for building production-ready Orbis plugins.
Code Organization
SDK-Based Project Structure
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
// 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::*; // 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); // 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)
} // 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
// 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
// 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
// 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
// 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
// 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
{
"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
| Element | Convention | Example |
|---|---|---|
| Page IDs | kebab-case | item-list |
| State paths | camelCase | selectedItem |
| Route paths | kebab-case | /my-plugin/items |
| Action types | snake_case | update_state |
| Component types | PascalCase | StatCard |
Reusable Layouts
Define common patterns:
{
"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
{
"events": {
"on_click": [
{
"type": "update_state",
"path": "ui.selectedId",
"value": "{{$row.id}}"
}
]
}
} ❌ Avoid updating entire objects:
{
"type": "update_state",
"path": "data",
"value": { "...all data plus changes..." }
} Debounce Expensive Operations
{
"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:
{
"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:
{
"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
{
"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" }
]
}
]
}
} {
"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
{
"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:
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:
{
"permissions": ["database:read"]
} ❌ Avoid over-permissioning:
{
"permissions": ["database:read", "database:write", "filesystem", "network"]
} Secure API Calls
{
"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:
{
"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 }
]
}
]
}
} {
"type": "Conditional",
"condition": "{{state.$loading}}",
"then": { "type": "LoadingOverlay" }
} Empty States
Handle empty data gracefully:
{
"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:
{
"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
{
"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
{
"type": "Section",
"ariaLabel": "User Statistics",
"children": [
{ "type": "Heading", "level": 2, "text": "Statistics" },
{ "...content..." }
]
} Form Labels
Always provide labels:
{
"name": "email",
"fieldType": "text",
"label": "Email Address",
"placeholder": "[email protected]",
"required": true
} Focus Management
{
"type": "Modal",
"autoFocus": true,
"returnFocus": true,
"events": {
"on_close": [{ "type": "update_state", "path": "modalOpen", "value": false }]
}
} Keyboard Navigation
Ensure interactive elements are keyboard accessible:
{
"type": "Button",
"label": "Action",
"ariaLabel": "Perform main action",
"events": {
"on_click": [{ "...actions..." }]
}
} Maintenance
Versioning
Follow semantic versioning:
{
"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:
# 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:
# 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
| Check | Threshold |
|---|---|
| Test coverage | > 80% |
| WASM size | < 200KB |
| Load time | < 100ms |
| No console errors | 0 |
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,formgroups -
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
- Components - UI component reference
- Actions - Action reference
- API Reference - Technical details
On This Page
- Code Organization
- SDK-Based Project Structure
- Module Separation with SDK
- SDK Best Practices
- Schema Design
- State Organization
- Naming Conventions
- Reusable Layouts
- Performance
- Minimize State Updates
- Debounce Expensive Operations
- Lazy Loading
- Optimize Lists
- Error Handling
- Graceful Degradation
- Validation Feedback
- Error Boundaries
- Security
- Input Validation
- Permission Scoping
- Secure API Calls
- User Experience
- Loading States
- Empty States
- Feedback Actions
- Confirm Destructive Actions
- Accessibility
- Semantic Structure
- Form Labels
- Focus Management
- Keyboard Navigation
- Maintenance
- Versioning
- Changelog
- Documentation
- Checklist
- Before Release
- Quality Gates
- Anti-Patterns
- Avoid
- Prefer
- Next Steps