Skip to main content
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:
  1. Time waste: Writing list, create, edit pages all by hand
  2. Lack of consistency: Different styles and structures per developer
  3. Hard to maintain: Need to modify all views when entity changes
  4. Prone to mistakes: Missing fields, wrong types, missing validation
  5. 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:
  1. ✨ Instant generation: Generate views with just clicks in web UI
  2. ✨ Type safe: Entity types directly reflected in components
  3. ✨ Consistent UI: Unified design system
  4. ✨ Best practices: Proven patterns applied
  5. ✨ 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:
  1. Access Sonamu UI
  2. Select β€œScaffold” menu
  3. Select Entity (e.g., User)
  4. Click β€œGenerate View List” button β†’ index.tsx generated
  5. 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

1. Git Commit Immediately After Generation

# Generate views in Sonamu UI
git add web/src/routes/admin/users/
git commit -m "feat: scaffold User views"
This allows tracking customization history.

2. Extract Common Logic into Hooks

// 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