Learn how to automatically generate list and form views for admin pages with Sonamuβs scaffolding system.
View Scaffolding Overview
Auto Generation
List/Form viewsGenerate via web UI
Type Safe
Entity basedCompile-time validation
Customizable
Modify after generationFull control
Consistency
Unified UI/UXBest practices
What is View Scaffolding?
Problem: Repetitive CRUD View Writing
In traditional development, you have to manually write similar CRUD screens repeatedly for each entity.
Problems with manual writing:
- Time waste: Writing list, create, edit pages all by hand
- Lack of consistency: Different styles and structures per developer
- Hard to maintain: Need to modify all views when entity changes
- Prone to mistakes: Missing fields, wrong types, missing validation
- Boilerplate: Repetitive code for tables, forms, buttons
Example: Pages needed for a single User entity
/admin/users β List page (table, pagination, search)
/admin/users/form β Create/Edit page (form, validation)
Each page requires 100-200+ lines of code, and this must be repeated for every entity.
Solution: Auto Scaffolding
Sonamu automatically generates admin page views based on Entity definitions.
Benefits:
- β¨ Instant generation: Generate views with just clicks in web UI
- β¨ Type safe: Entity types directly reflected in components
- β¨ Consistent UI: Unified design system
- β¨ Best practices: Proven patterns applied
- β¨ Customizable: Modify only necessary parts after generation
Sonamuβs scaffolding philosophy:
β80% automatic, 20% customizationβ
Most CRUD functionality is auto-generated, and only special business logic needs additional implementation.
Running Scaffolding
Using Sonamu Web UI
Sonamu provides a web-based management UI.
# Run Sonamu dev server
pnpm dev
# Access Sonamu UI in browser
# http://localhost:3000/sonamu
Generating views in web UI:
- Access Sonamu UI
- Select βScaffoldβ menu
- Select Entity (e.g., User)
- Click βGenerate View Listβ button β
index.tsx generated
- Click βGenerate View Formβ button β
form.tsx generated
CLI commands are currently disabled. View generation is only available through the Sonamu web UI.
Generated File Structure
web/src/routes/admin/
βββ users/
βββ index.tsx # List page (includes table)
βββ form.tsx # Integrated create/edit form
Actually generated files:
- index.tsx: Page with list table directly included
- form.tsx: Create and edit combined into one form
Note:
- Separate component files (UserTable.tsx, UserForm.tsx) are not generated
- Detail page ([id].tsx) is not auto-generated
- All logic is integrated into two page files
Generated View Structure
List Page (index.tsx)
Displays list in a table and provides pagination and search functionality.
// web/src/routes/admin/users/index.tsx (auto-generated)
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { UserService } from "@/services/services.generated";
export default function UsersPage() {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
// Using TanStack Query Hook
const { data: users, isLoading } = UserService.useUsers("A", {
page,
pageSize: 20,
search,
});
return (
<div className="admin-page">
<div className="header">
<h1>Users</h1>
<button onClick={() => navigate("/admin/users/form")}>
Create New User
</button>
</div>
<div className="search">
<input
type="text"
placeholder="Search users..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{/* Table directly included */}
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Username</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users?.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.email}</td>
<td>{user.username}</td>
<td>
<button onClick={() => navigate(`/admin/users/form?id=${user.id}`)}>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div className="pagination">
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(page + 1)}>
Next
</button>
</div>
</div>
);
}
Features:
- Pagination
- Search filter
- βCreate Newβ button
- βEditβ button (each row)
- Table directly included in page (no separate component)
Form Page (form.tsx)
Combines create and edit into one form.
// web/src/routes/admin/users/form.tsx (auto-generated)
import { useState, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { UserService } from "@/services/services.generated";
export default function UserFormPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const userId = searchParams.get("id");
const [formData, setFormData] = useState({
email: "",
username: "",
bio: "",
});
const [saving, setSaving] = useState(false);
// Edit mode: Load existing data
useEffect(() => {
if (userId) {
UserService.getUser("C", parseInt(userId)).then((user) => {
setFormData({
email: user.email,
username: user.username,
bio: user.bio || "",
});
});
}
}, [userId]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
try {
if (userId) {
// Update
await UserService.update(parseInt(userId), formData);
} else {
// Create
await UserService.create(formData);
}
navigate("/admin/users");
} catch (error) {
alert("Failed to save");
} finally {
setSaving(false);
}
}
return (
<div className="admin-page">
<div className="header">
<h1>{userId ? "Edit User" : "Create New User"}</h1>
<button onClick={() => navigate("/admin/users")}>
Back to List
</button>
</div>
<form onSubmit={handleSubmit} className="entity-form">
<div className="form-field">
<label>Email *</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
<div className="form-field">
<label>Username *</label>
<input
type="text"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
required
/>
</div>
<div className="form-field">
<label>Bio</label>
<textarea
value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
rows={4}
/>
</div>
<div className="form-actions">
<button type="submit" disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
</div>
</form>
</div>
);
}
Features:
- Create/Edit integrated: Mode distinguished by URL parameter (
?id=)
- Auto-loads existing data when editing
- Validation
- Button disabled while saving
- βBack to Listβ button
Generated File Characteristics
Integrated Structure
Sonamuβs scaffolding consists of minimal files:
Traditional structure (complex):
pages/users/
βββ index.tsx
βββ [id].tsx
βββ new.tsx
βββ [id]/edit.tsx
components/users/
βββ UserTable.tsx
βββ UserDetail.tsx
βββ UserForm.tsx
Sonamu structure (simple):
admin/users/
βββ index.tsx # Includes table
βββ form.tsx # Create/Edit integrated
Benefits of integrated structure:
- Minimized file count
- Less navigation between files
- Easy maintenance
- High code cohesion
Routing Pattern
GET /admin/users β index.tsx (list)
GET /admin/users/form β form.tsx (create)
GET /admin/users/form?id=1 β form.tsx (edit)
Create/edit mode is distinguished by URL parameters.
Customization
Modify After Generation
Generated files are fully modifiable.
// web/src/routes/admin/users/index.tsx (customized)
export default function UsersPage() {
// Added: Role filter
const [role, setRole] = useState<string | null>(null);
const { data: users } = UserService.useUsers("A", {
page,
pageSize: 20,
search,
role, // Custom parameter
});
return (
<div>
{/* Added: Role filter UI */}
<select value={role || ""} onChange={(e) => setRole(e.target.value || null)}>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
{/* Existing table... */}
</div>
);
}
Separate into Components
You can manually separate components if needed:
// components/users/UserTable.tsx (manually created)
export function UserTable({ users }: { users: User[] }) {
return (
<table className="data-table">
{/* Table content */}
</table>
);
}
// admin/users/index.tsx (modified)
import { UserTable } from "@/components/users/UserTable";
export default function UsersPage() {
const { data: users } = UserService.useUsers("A", { page, pageSize: 20 });
return (
<div>
<UserTable users={users || []} />
</div>
);
}
Add Detail Page
Add detail page manually since itβs not auto-generated:
// admin/users/[id].tsx (manually created)
import { useParams } from "react-router-dom";
import { UserService } from "@/services/services.generated";
export default function UserDetailPage() {
const { id } = useParams();
const { data: user } = UserService.useUser("C", parseInt(id!));
return (
<div>
<h1>{user?.username}</h1>
<p>{user?.email}</p>
<p>{user?.bio}</p>
</div>
);
}
Best Practices
# Generate views in Sonamu UI
git add web/src/routes/admin/users/
git commit -m "feat: scaffold User views"
This allows tracking customization history.
// hooks/useEntityList.ts
export function useEntityList<T>(
entityName: string,
useHook: any
) {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const { data, isLoading } = useHook("A", {
page,
pageSize: 20,
search,
});
return { data, isLoading, page, setPage, search, setSearch };
}
3. Manage Styles Globally
/* styles/admin.css */
.admin-page { }
.data-table { }
.entity-form { }
Use consistent styles across all admin pages.
Cautions
Cautions when using View Scaffolding:
- Only available via web UI (CLI commands disabled)
- Only index.tsx, form.tsx generated (no separate components)
- Detail page needs to be added manually
- Regenerating will lose customizations (Git commit required)
- Consider separating components as needed after generation
Next Steps