Integration with @xond/api
@xond/ui is designed to work seamlessly with code generated by @xond/api. This integration provides type-safe, data-driven UIs with minimal boilerplate.
Overview
When you generate code with @xond/api, it creates:
- Backend Services – REST API endpoints (
*Service.ts) - Frontend Models – TypeScript models with column definitions (
*Model.tsx) - Model Registry – Centralized registry of all models
@xond/ui components consume these generated models to automatically render forms, tables, and views.
Generated Models Structure
After running xond-api generate, you'll have:
src/generated/models/
├── userModel.tsx
├── productModel.tsx
├── index.ts
└── ...
Model File Structure
// userModel.tsx
export interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
export interface UserColumnModel {
id: { label: string; type: string; required: boolean };
name: { label: string; type: string; required: boolean };
email: { label: string; type: string; required: boolean };
createdAt: { label: string; type: string; required: boolean };
}
export const UserModel: UserColumnModel = {
id: { label: "ID", type: "uuid", required: true },
name: { label: "Name", type: "string", required: true },
email: { label: "Email", type: "string", required: true },
createdAt: { label: "Created At", type: "datetime", required: false },
};
export const UserColumnModel: UserColumnModel = UserModel;
Model Registry
// index.ts
export * from "./userModel";
export * from "./productModel";
export const modelRegistry = {
user: UserModel,
product: ProductModel,
};
export const columnModelRegistry = {
user: UserColumnModel,
product: ProductColumnModel,
};
Using Generated Models
With Table Component
import { Table } from "@xond/ui";
import { useRemoteDataSource } from "@xond/ui";
import { modelRegistry, columnModelRegistry } from "@your-app/generated/models";
function UsersTable() {
const UserModel = modelRegistry.user;
const UserColumns = columnModelRegistry.user;
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
});
return (
<Table
dataSource={dataSource}
columns={UserColumns}
model={UserModel}
/>
);
}
With Form Component
Forms can be generated automatically from models, or you can customize the form model:
import { Form } from "@xond/ui";
import { useRemoteDataSource } from "@xond/ui";
import { modelRegistry } from "@your-app/generated/models";
function UserForm() {
const UserModel = modelRegistry.user;
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
});
// Convert model to form model
const formModel = Object.entries(UserModel).map(([fieldName, config]) => ({
fieldName,
fieldType: mapTypeToInputType(config.type),
label: config.label,
isRequired: config.required,
}));
return (
<Form
formModel={formModel}
dataSource={dataSource}
/>
);
}
API Integration
Backend Service Structure
Generated services follow a consistent pattern:
// Generated service
export const getUsers = async (
prisma: PrismaClient,
params: {
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: "asc" | "desc";
filters?: Record<string, any>;
}
) => {
// Implementation
};
Using with RemoteDataSource
RemoteDataSource automatically formats requests to match generated service signatures:
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users", // Points to your Express route
pageSize: 20,
initialSortConfig: [{ field: "name", direction: "asc" }],
});
// DataSource automatically calls:
// GET /api/users?page=1&pageSize=20&sortBy=name&sortOrder=asc
Express Route Integration
Your Express routes should use the generated services:
import { serviceRegistry } from "./modules/json/services/generated/serviceRegistry";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
app.get("/api/users", async (req, res) => {
const users = await serviceRegistry["user"].getUsers(prisma, {
page: parseInt(req.query.page as string) || 1,
pageSize: parseInt(req.query.pageSize as string) || 10,
sortBy: req.query.sortBy as string,
sortOrder: req.query.sortOrder as "asc" | "desc",
filters: req.query.filters ? JSON.parse(req.query.filters as string) : {},
});
res.json(users);
});
Complete Integration Example
Here's a complete example showing the full integration:
1. Backend Route
// apps/your-api/src/routes/users.ts
import express from "express";
import { serviceRegistry } from "../modules/json/services/generated/serviceRegistry";
import { PrismaClient } from "@prisma/client";
const router = express.Router();
const prisma = new PrismaClient();
router.get("/", async (req, res) => {
try {
const result = await serviceRegistry["user"].getUsers(prisma, {
page: parseInt(req.query.page as string) || 1,
pageSize: parseInt(req.query.pageSize as string) || 10,
sortBy: req.query.sortBy as string,
sortOrder: req.query.sortOrder as "asc" | "desc",
});
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.post("/", async (req, res) => {
try {
const result = await serviceRegistry["user"].createUser(prisma, req.body);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;
2. Frontend Component
// apps/your-ui/src/pages/Users.tsx
import { Table, Form, Button } from "@xond/ui";
import { useRemoteDataSource } from "@xond/ui";
import { modelRegistry, columnModelRegistry } from "@your-app/generated/models";
export function UsersPage() {
const UserModel = modelRegistry.user;
const UserColumns = columnModelRegistry.user;
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
onAfterSave: async () => {
dataSource.load(); // Reload after save
},
});
// Convert model to form model
const formModel = Object.entries(UserModel).map(([fieldName, config]) => ({
fieldName,
fieldType: mapTypeToInputType(config.type),
label: config.label,
isRequired: config.required,
isNullable: !config.required,
}));
return (
<div>
<Button onClick={() => dataSource.startAdd()}>Add User</Button>
<Table
dataSource={dataSource}
columns={UserColumns}
model={UserModel}
onRowClick={(row) => dataSource.startEdit(row)}
/>
{dataSource.formMode && (
<Form
formModel={formModel}
dataSource={dataSource}
onClose={() => dataSource.startView(null)}
/>
)}
</div>
);
}
function mapTypeToInputType(type: string): string {
const typeMap: Record<string, string> = {
uuid: "UUIDInput",
string: "TextInput",
number: "NumberInput",
datetime: "DateInput",
boolean: "ToggleInput",
email: "EmailInput",
phone: "PhoneInput",
};
return typeMap[type] || "TextInput";
}
Type Safety
The integration provides full type safety:
import { User } from "@your-app/generated/models";
// Type-safe data
const users: User[] = dataSource.data;
// Type-safe form data
const formData: User = dataSource.formData;
// Type-safe columns
const columns = columnModelRegistry.user; // Fully typed
Customization
You can customize generated models:
import { modelRegistry } from "@your-app/generated/models";
const UserModel = modelRegistry.user;
// Customize form model
const customFormModel = [
...Object.entries(UserModel)
.filter(([field]) => field !== "id") // Exclude ID
.map(([fieldName, config]) => ({
fieldName,
fieldType: mapTypeToInputType(config.type),
label: config.label,
isRequired: config.required,
})),
// Add custom fields
{
fieldName: "confirmPassword",
fieldType: "PasswordInput",
label: "Confirm Password",
validation: {
validate: (value, formValues) =>
value === formValues.password || "Passwords do not match",
},
},
];
Best Practices
- Use Generated Models – Always use models from
@xond/apigeneration - Keep Models in Sync – Regenerate models when database schema changes
- Type Safety – Leverage TypeScript types from generated models
- Customize When Needed – Extend form models with custom fields and validation
- Consistent Endpoints – Use consistent endpoint naming (
/api/{resourceName}) - Error Handling – Handle errors from DataSource in your components
Next Steps
- Learn about data sources
- Explore forms and validation
- See component examples