| title | Object UI Best Practices |
|---|
This guide covers best practices for building applications with Object UI's JSON-driven approach.
- Schema Design
- Performance
- Type Safety
- Maintainability
- API Integration
- Security
- Testing
- Accessibility
❌ Bad:
{
"type": "div",
"body": [
{
"type": "card",
"title": "User 1",
"body": [
{ "type": "text", "content": "Name: John" },
{ "type": "text", "content": "Email: john@example.com" }
]
},
{
"type": "card",
"title": "User 2",
"body": [
{ "type": "text", "content": "Name: Jane" },
{ "type": "text", "content": "Email": "jane@example.com" }
]
}
]
}✅ Good:
{
"type": "div",
"dataSource": {
"api": "/api/users"
},
"body": {
"type": "grid",
"columns": 2,
"children": "${data.map(user => ({ type: 'card', title: user.name, body: [{ type: 'text', content: user.email }] }))}"
}
}Choose the most appropriate component type for your content.
❌ Bad:
{
"type": "div",
"className": "border p-4",
"body": "This is a warning"
}✅ Good:
{
"type": "alert",
"variant": "warning",
"description": "This is a warning"
}Use visibleOn, hiddenOn, and disabledOn for dynamic UIs.
✅ Good:
{
"type": "button",
"label": "Delete",
"variant": "destructive",
"visibleOn": "${user.role === 'admin'}",
"disabledOn": "${item.status === 'locked'}"
}Group related fields and use clear labels.
✅ Good:
{
"type": "form",
"fields": [
{
"name": "personalInfo",
"type": "group",
"label": "Personal Information",
"fields": [
{ "name": "firstName", "type": "input", "label": "First Name" },
{ "name": "lastName", "type": "input", "label": "Last Name" }
]
},
{
"name": "contactInfo",
"type": "group",
"label": "Contact Information",
"fields": [
{ "name": "email", "type": "input", "inputType": "email", "label": "Email" },
{ "name": "phone", "type": "input", "inputType": "tel", "label": "Phone" }
]
}
]
}❌ Bad - Fetch data in every component:
{
"type": "div",
"body": [
{
"type": "card",
"dataSource": { "api": "/api/stats" },
"body": "..."
},
{
"type": "card",
"dataSource": { "api": "/api/stats" },
"body": "..."
}
]
}✅ Good - Fetch once at parent level:
{
"type": "div",
"dataSource": { "api": "/api/stats" },
"body": [
{
"type": "card",
"body": "${data.users}"
},
{
"type": "card",
"body": "${data.orders}"
}
]
}{
"dataSource": {
"api": "/api/countries",
"cache": {
"key": "countries-list",
"duration": 3600000,
"staleWhileRevalidate": true
}
}
}{
"type": "crud",
"api": "/api/users",
"pagination": {
"enabled": true,
"pageSize": 20,
"pageSizeOptions": [10, 20, 50]
}
}{
"dataSource": {
"api": "/api/dashboard/stats",
"pollInterval": 30000,
"fetchOnMount": true
}
}✅ Good:
import { FormBuilder, input } from '@object-ui/core/builder';
const loginForm = new FormBuilder()
.field({
name: 'email',
type: 'input',
inputType: 'email',
required: true
})
.field({
name: 'password',
type: 'input',
inputType: 'password',
required: true
})
.submitLabel('Login')
.build();import { validateSchema, assertValidSchema } from '@object-ui/core/validation';
// Validate and get detailed errors
const result = validateSchema(schema);
if (!result.valid) {
console.error('Schema errors:', result.errors);
}
// Or assert and throw on error
assertValidSchema(schema);{
"$schema": "https://objectui.org/schema/v1",
"type": "page",
"body": [...]
}✅ Good:
{
"type": "button",
"id": "submit-login-button",
"testId": "login-submit",
"label": "Login"
}{
"type": "text",
"content": "${data.users.filter(u => u.active && u.verified).length}",
"description": "Count of active and verified users"
}Instead of:
{
"api": "/api/v1/users",
"operations": {
"create": { "api": "/api/v1/users" },
"update": { "api": "/api/v1/users/${id}" }
}
}Use environment variables or configuration:
{
"api": "${env.API_BASE}/users",
"operations": {
"create": { "api": "${env.API_BASE}/users" },
"update": { "api": "${env.API_BASE}/users/${id}" }
}
}Split large schemas into multiple files:
// schemas/users/list.json
// schemas/users/form.json
// schemas/users/detail.json
import userList from './schemas/users/list.json';
import userForm from './schemas/users/form.json';✅ Good:
{
"onClick": {
"type": "api",
"api": {
"request": {
"url": "/api/action",
"method": "POST"
},
"successMessage": "Action completed successfully!",
"errorMessage": "Failed to complete action. Please try again.",
"showLoading": true
}
}
}✅ Good:
{
"type": "action",
"label": "Delete",
"level": "danger",
"confirmText": "Are you sure you want to delete this item? This action cannot be undone.",
"api": "/api/items/${id}",
"method": "DELETE"
}{
"api": {
"request": {
"url": "/api/save",
"method": "POST"
},
"showLoading": true,
"successMessage": "Changes saved successfully!",
"errorMessage": "Failed to save changes",
"reload": true
}
}✅ Good:
{
"dataSource": {
"api": "/api/products",
"transform": "data => data.products.map(p => ({ ...p, displayName: `${p.name} (${p.sku})` }))"
}
}❌ Bad:
{
"api": {
"url": "/api/users",
"headers": {
"Authorization": "******"
}
}
}✅ Good:
{
"api": {
"url": "/api/users",
"headers": {
"Authorization": "${env.API_TOKEN}"
}
}
}{
"name": "email",
"type": "input",
"inputType": "email",
"required": true,
"validation": {
"pattern": {
"value": "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$",
"message": "Please enter a valid email address"
}
}
}{
"api": "https://api.example.com/data"
}When displaying user-generated content, use appropriate sanitization.
{
"type": "button",
"testId": "submit-form",
"label": "Submit"
}Then in tests:
await page.getByTestId('submit-form').click();import { validateSchema } from '@object-ui/core/validation';
test('schema is valid', () => {
const result = validateSchema(mySchema);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});test('expression evaluates correctly', () => {
const schema = {
type: 'text',
content: '${user.name}'
};
const context = { user: { name: 'John' } };
const result = evaluateExpression(schema.content, context);
expect(result).toBe('John');
});✅ Good:
{
"type": "button",
"label": "Close",
"ariaLabel": "Close dialog",
"icon": "x"
}{
"type": "image",
"src": "/logo.png",
"alt": "Company Logo"
}{
"type": "dialog",
"closeOnEscape": true,
"showClose": true
}Use Tailwind's semantic color classes for proper contrast:
{
"type": "button",
"className": "bg-primary text-primary-foreground"
}{
"type": "card",
"className": "shadow-lg",
"body": [
{
"type": "flex",
"justify": "between",
"align": "start",
"body": [
{
"type": "div",
"body": [
{
"type": "text",
"content": "Total Users",
"className": "text-sm font-medium text-muted-foreground"
},
{
"type": "text",
"content": "${data.userCount}",
"className": "text-3xl font-bold"
}
]
},
{
"type": "icon",
"name": "users",
"className": "text-muted-foreground"
}
]
}
]
}{
"type": "crud",
"resource": "user",
"api": "/api/users",
"columns": [...],
"rowActions": [
{
"type": "action",
"label": "Edit",
"icon": "edit"
},
{
"type": "action",
"label": "Delete",
"icon": "trash",
"level": "danger",
"confirmText": "Delete this user?"
}
]
}{
"type": "tabs",
"items": [
{
"value": "step1",
"label": "Personal Info",
"content": {
"type": "form",
"fields": [...]
}
},
{
"value": "step2",
"label": "Contact Info",
"content": {
"type": "form",
"fields": [...]
}
}
]
}- ✅ Keep schemas modular and reusable
- ✅ Use semantic component types
- ✅ Leverage conditional rendering
- ✅ Optimize data fetching and caching
- ✅ Use TypeScript for type safety
- ✅ Validate schemas before runtime
- ✅ Handle errors gracefully
- ✅ Never expose sensitive data
- ✅ Add test IDs for testing
- ✅ Follow accessibility guidelines
Following these best practices will help you build maintainable, performant, and accessible applications with Object UI.