Skip to main content

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:

  1. Backend Services – REST API endpoints (*Service.ts)
  2. Frontend Models – TypeScript models with column definitions (*Model.tsx)
  3. 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

  1. Use Generated Models – Always use models from @xond/api generation
  2. Keep Models in Sync – Regenerate models when database schema changes
  3. Type Safety – Leverage TypeScript types from generated models
  4. Customize When Needed – Extend form models with custom fields and validation
  5. Consistent Endpoints – Use consistent endpoint naming (/api/{resourceName})
  6. Error Handling – Handle errors from DataSource in your components

Next Steps