Learn how to automatically generate list and form views for admin pages with Sonamuβs scaffolding system.
View Scaffolding Overview
Auto Generation List/Form views Generate via web UI
Type Safe Entity based Compile-time validation
Customizable Modify after generation Full control
Consistency Unified UI/UX Best 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 : 1. Only available via web UI (CLI commands disabled)
2. Only index.tsx, form.tsx generated (no separate components) 3. Detail page needs to be
added manually 4. Regenerating will lose customizations (Git commit required) 5.
Consider separating components as needed after generation
Next Steps
Custom Components Component customization
ID Async Select Relational data selection
Search Input Search component
Using Services Service integration