Form Wrapper Components
Form wrapper components are server-rendered Astro components that handle data fetching, authorization, error handling, and loading states for CRUD forms. They eliminate code duplication and provide a consistent pattern across all dashboard forms.
Why Use Form Wrappers?
Section titled “Why Use Form Wrappers?”Before Form Wrappers
Section titled “Before Form Wrappers”---// ~130 lines of boilerplate per pageimport { getDatabase } from '#libs/database'import { logger } from '#utils/logger'
const csrfToken = Astro.locals.csrfToken ?? ''let client: Client | null = nulllet error: string | undefined
try { const db = getDatabase() const userDbId = await db.getUserIdByClerkId(Astro.locals.userId) client = await db.getClientById(clientId, userDbId) // ... more authorization logic ...} catch (err) { error = 'Failed to load client' await logger.error('Error', { error: err.message })}---
{error ? ( <Alert severity="error">{error}</Alert>) : ( <ClientFormRHF client:load csrfToken={csrfToken} client={client} />)}After Form Wrappers
Section titled “After Form Wrappers”---// ~25 lines - all logic handled by wrapperimport ClientFormWrapper from '#components/astro/forms/ClientFormWrapper.astro'---
<ClientFormWrapper clientId={clientId} />Available Form Wrappers
Section titled “Available Form Wrappers”ClientFormWrapper
Section titled “ClientFormWrapper”For managing client records (create/edit).
---import ClientFormWrapper from '#components/astro/forms/ClientFormWrapper.astro'---
<!-- Create mode --><ClientFormWrapper />
<!-- Edit mode --><ClientFormWrapper clientId={clientId} />Authorization:
- Create: No restrictions
- Edit: Owner or
super_adminrole
EventFormWrapper
Section titled “EventFormWrapper”For managing events (create/edit, including recurring events).
---import EventFormWrapper from '#components/astro/forms/EventFormWrapper.astro'---
<!-- Create mode --><EventFormWrapper />
<!-- Edit mode --><EventFormWrapper eventId={eventId} />Authorization:
- Create:
adminrole required - Edit: Owner or
adminrole
OrderFormWrapper
Section titled “OrderFormWrapper”For managing orders (create/edit).
---import OrderFormWrapper from '#components/astro/forms/OrderFormWrapper.astro'---
<!-- Create mode --><OrderFormWrapper />
<!-- Edit mode --><OrderFormWrapper orderId={orderId} />Special Features:
- Auto-fetches clients list for dropdown
- Caches clients list for request duration (prevents duplicate fetches)
Authorization:
- Create: No restrictions
- Edit: Owner or
super_adminrole
ProductFormWrapper
Section titled “ProductFormWrapper”For managing products (create/edit).
---import ProductFormWrapper from '#components/astro/forms/ProductFormWrapper.astro'---
<!-- Create mode --><ProductFormWrapper />
<!-- Edit mode --><ProductFormWrapper productId={productId} />Authorization:
- Create: No restrictions
- Edit: Owner or
super_adminrole
Basic Usage
Section titled “Basic Usage”Create Form Page
Section titled “Create Form Page”---import Auth from '#layouts/Auth.astro'import { Row, Col } from '@fpkit/acss'import ClientFormWrapper from '#components/astro/forms/ClientFormWrapper.astro'import ClientFormHelp from '#components/astro/forms/help/ClientFormHelp.astro'---
<Auth pageTitle="Create Client"> <Row as="section" gap="xl" className="container"> <Col span={8}> <h1 class="text-3xl font-semibold mb-6">Create Client</h1> <ClientFormWrapper /> </Col> <Col span={4}> <ClientFormHelp /> </Col> </Row></Auth>Edit Form Page
Section titled “Edit Form Page”---import Auth from '#layouts/Auth.astro'import { Row, Col } from '@fpkit/acss'import ClientFormWrapper from '#components/astro/forms/ClientFormWrapper.astro'import ClientFormHelp from '#components/astro/forms/help/ClientFormHelp.astro'
const clientId = Astro.params.id
if (!clientId) { return Astro.redirect('/dashboard/clients')}---
<Auth pageTitle="Edit Client"> <Row as="section" gap="xl" className="container"> <Col span={8}> <h1 class="text-3xl font-semibold mb-6">Edit Client</h1> <ClientFormWrapper clientId={clientId} /> </Col> <Col span={4}> <ClientFormHelp /> </Col> </Row></Auth>Advanced Features
Section titled “Advanced Features”Custom Error Messages
Section titled “Custom Error Messages”Customize error messages shown to users:
<OrderFormWrapper orderId={orderId} errorMessages={{ notFound: "This order doesn't exist or has been deleted.", unauthorized: "You don't have permission to edit this order.", fetchFailed: "Failed to load order data. Please try again." }}/>Custom Loading Message
Section titled “Custom Loading Message”Show a custom message during server-side data fetch:
<ProductFormWrapper productId={productId} loadingMessage="Loading product details..."/>Disable Loading Skeleton
Section titled “Disable Loading Skeleton”Skip the loading skeleton and show errors only:
<EventFormWrapper eventId={eventId} showLoadingSkeleton={false}/>Custom Authorization
Section titled “Custom Authorization”Override default authorization behavior:
<!-- Allow team_admin to edit without ownership --><OrderFormWrapper orderId={orderId} requiredRoles={['super_admin', 'team_admin']}/>
<!-- Skip ownership check entirely (for admins) --><ClientFormWrapper clientId={clientId} skipOwnershipCheck={true}/>Pre-Fetched Data
Section titled “Pre-Fetched Data”Pass pre-fetched data to skip wrapper’s server-side fetch:
---// Fetch data in page for custom logicconst db = getDatabase()const order = await db.getOrderById(orderId)const clients = await db.getClients({ limit: 1000 })---
<!-- Pass pre-fetched data --><OrderFormWrapper orderId={orderId} order={order} clients={clients}/>Help Components
Section titled “Help Components”Each form wrapper has a corresponding help component with usage guidance:
| Wrapper | Help Component | Purpose |
|---|---|---|
| ClientFormWrapper | ClientFormHelp | Client form guidance |
| EventFormWrapper | — | The event pages carry an upcoming-events rail instead of a help sidebar; field guidance lives in each field’s hint text |
| OrderFormWrapper | OrderFormHelp | Order form guidance (services, status) |
| ProductFormWrapper | — | The product pages carry a recent-products rail instead of a help sidebar; field guidance lives in each field’s hint text |
Example Help Component Usage
Section titled “Example Help Component Usage”---import ClientFormHelp from '#components/astro/forms/help/ClientFormHelp.astro'---
<Row as="section" gap="xl" className="container"> <Col span={8}> <!-- Form column --> <ClientFormWrapper /> </Col> <Col span={4}> <!-- Help sidebar --> <ClientFormHelp /> </Col></Row>Security Features
Section titled “Security Features”Environment-Aware Error Messages
Section titled “Environment-Aware Error Messages”Form wrappers protect sensitive information by showing different error messages based on environment:
Development Mode (local development):
- Detailed error messages with stack traces
- Database errors show table names and constraints
- Useful for debugging and troubleshooting
Production Mode (deployed application):
- Generic user-safe error messages
- No database schema or SQL syntax exposed
- Prevents reconnaissance attacks
Implementation Pattern:
try { // Fetch data} catch (err) { const isDev = import.meta.env.DEV const detailedError = err instanceof Error ? err.message : 'Failed to load client' error = errorMessages.fetchFailed ?? (isDev ? detailedError : 'Failed to load client. Please try again.')
// Full error details always logged server-side await logger.error('Failed to fetch client', { error: err instanceof Error ? err.message : 'Unknown error', stack: err instanceof Error ? err.stack : undefined })}Defense in Depth: User-safe messages + comprehensive server logs
Manual Ownership Validation
Section titled “Manual Ownership Validation”Form wrappers validate ownership using manual post-fetch verification:
Pattern (Client, Event, Product wrappers):
// 1. Fetch entity by ID (no user constraint)const client = await db.getClientById(clientId)
if (client) { // 2. Manually check ownership const clientCreatedBy = client.created_by const isOwner = clientCreatedBy === userDbId const canBypass = skipOwnershipCheck || (Astro.locals.userRole && requiredRoles.includes(Astro.locals.userRole))
// 3. Enforce authorization if (!isOwner && !canBypass) { client = null error = 'You do not have permission to edit this client.' await logger.warn('Client access denied - ownership check failed') }}Why Manual Validation?:
- Application-layer security check
- Clear audit trail in logs
- Flexible authorization logic
- Consistent across all wrappers
Order Wrapper Exception:
- Uses built-in ownership parameter:
db.getOrderById(orderId, userDbId) - Simpler pattern when database method supports it
How It Works
Section titled “How It Works”Server-Side Rendering (SSR)
Section titled “Server-Side Rendering (SSR)”Form wrappers fetch data during server-side rendering (SSR), not client-side:
1. Request arrives at server2. Wrapper fetches data from database (~20-100ms)3. Loading skeleton shown during fetch (SSR)4. Form rendered with data5. Page sent to client (no client-side loading needed)Authorization Flow
Section titled “Authorization Flow”For edit mode, wrappers check authorization before rendering the form:
1. Check authentication (via Clerk)2. Get user's database ID3. Try ownership-based fetch (user owns entity?)4. If not found, check required roles (e.g., super_admin)5. If authorized, render form6. If unauthorized, show error alertRequest-Scoped Caching
Section titled “Request-Scoped Caching”OrderFormWrapper caches the clients list to prevent duplicate fetches:
First OrderFormWrapper on page:└─ Fetch clients from database└─ Cache in Astro.locals.cachedClients
Second OrderFormWrapper on page:└─ Use cached clients (no fetch)
After request completes:└─ Cache cleared automaticallyCommon Props
Section titled “Common Props”All form wrappers share these common props:
Mode Detection
Section titled “Mode Detection”{ // Entity ID for edit mode (auto-detects edit mode if provided) clientId?: string // ClientFormWrapper eventId?: string // EventFormWrapper orderId?: string // OrderFormWrapper productId?: string // ProductFormWrapper
// Explicit mode override (usually not needed) mode?: 'create' | 'edit'}Configuration
Section titled “Configuration”{ // API endpoint override (auto-generated if not provided) apiEndpoint?: string
// Show error alerts (default: true) showErrors?: boolean
// Hydration strategy for React form (default: 'load') hydration?: 'load' | 'visible' | 'idle'
// Additional CSS class className?: string}Loading & Errors
Section titled “Loading & Errors”{ // Show loading skeleton during fetch (default: true) showLoadingSkeleton?: boolean
// Loading message (default: "Loading...") loadingMessage?: string
// Custom error messages errorMessages?: { notFound?: string unauthorized?: string fetchFailed?: string }}Authorization
Section titled “Authorization”{ // Required roles for edit without ownership (default: ['super_admin']) requiredRoles?: string[]
// Skip ownership check (default: false) skipOwnershipCheck?: boolean}Migration Guide
Section titled “Migration Guide”If you have existing form pages using the old pattern, here’s how to migrate:
Step 1: Identify the Form Type
Section titled “Step 1: Identify the Form Type”Determine which wrapper to use:
- Client CRUD? → ClientFormWrapper
- Event CRUD? → EventFormWrapper
- Order CRUD? → OrderFormWrapper
- Product CRUD? → ProductFormWrapper
Step 2: Remove Manual Data Fetching
Section titled “Step 2: Remove Manual Data Fetching”Delete lines that:
- Fetch CSRF token
- Fetch entity data (client/event/order/product)
- Check authorization
- Handle errors
- Show loading states
Step 3: Add Wrapper Component
Section titled “Step 3: Add Wrapper Component”Replace removed code with wrapper:
<!-- Before --><ClientFormRHF client:load csrfToken={csrfToken} client={client} apiEndpoint={`/api/clients/edit/${clientId}`}/>
<!-- After --><ClientFormWrapper clientId={clientId} />Step 4: Extract Help Content
Section titled “Step 4: Extract Help Content”Move help sidebar content to help component:
<!-- Before --><Col span={4}> <Title as="h2" size="lg">Client Information</Title> <p>Edit client details...</p> <!-- More help content --></Col>
<!-- After --><Col span={4}> <ClientFormHelp /></Col>Step 5: Test
Section titled “Step 5: Test”- ✅ Create mode works
- ✅ Edit mode pre-populates data
- ✅ Authorization enforced (owner or admin)
- ✅ Errors displayed correctly
- ✅ Loading skeleton shown (edit mode)
Troubleshooting
Section titled “Troubleshooting”Form Not Loading
Section titled “Form Not Loading”Problem: Blank page or loading skeleton persists.
Solutions:
- Check server logs for database errors
- Verify entity ID is valid
- Confirm user is authenticated (
Astro.locals.userId) - Verify database configuration
Error “Not Found or No Permission”
Section titled “Error “Not Found or No Permission””Problem: Error alert shows even though entity exists.
Solutions:
- Check user owns entity (database
user_idmatches) - Verify user has required role (
super_adminfor default) - Check server logs for authorization failures
CSRF Token Missing Warning
Section titled “CSRF Token Missing Warning”Problem: Warning in logs: “CSRF token missing in FormWrapper”.
Solutions:
- Verify middleware is running (
src/middleware.ts) - Confirm page uses Auth layout
- Check
Astro.locals.csrfTokenis set
Clients List Not Loading (Orders)
Section titled “Clients List Not Loading (Orders)”Problem: Order form shows “No clients available”.
Solutions:
- Verify clients exist in database
- Check query filters in OrderFormWrapper
- Restart dev server (cache is request-scoped)
Generic Error Messages in Production
Section titled “Generic Error Messages in Production”Problem: Production shows “Failed to load…” instead of specific errors.
This is Expected Behavior:
- Production uses generic messages to prevent information disclosure
- Development mode shows detailed errors for debugging
- Check server logs for full error details with correlation IDs
Solutions:
- Check server logs using correlation ID
- Use custom
errorMessagesprop for user-friendly production messages - In development, detailed errors shown automatically
Best Practices
Section titled “Best Practices”- Use wrappers for all CRUD forms - Consistent pattern across app
- Use help components - Provide guidance to users
- Customize error messages - Make errors user-friendly
- Test authorization - Verify ownership and role checks work
- Check server logs - Debug issues with structured logging
❌ DON’T
Section titled “❌ DON’T”- Don’t bypass wrappers - Defeats the purpose of centralization
- Don’t fetch data manually - Let wrappers handle it
- Don’t hardcode authorization - Use wrapper props instead
- Don’t skip testing - Authorization bugs are security issues
- Don’t ignore errors - They indicate real problems
Examples
Section titled “Examples”Minimal Create Form
Section titled “Minimal Create Form”---import Auth from '#layouts/Auth.astro'import ProductFormWrapper from '#components/astro/forms/ProductFormWrapper.astro'---
<Auth pageTitle="Create Product"> <section class="container"> <h1>Create Product</h1> <ProductFormWrapper /> </section></Auth>Full-Featured Edit Form
Section titled “Full-Featured Edit Form”---import Auth from '#layouts/Auth.astro'import { Row, Col } from '@fpkit/acss'import OrderFormWrapper from '#components/astro/forms/OrderFormWrapper.astro'import OrderFormHelp from '#components/astro/forms/help/OrderFormHelp.astro'
const orderId = Astro.params.id
if (!orderId) { return Astro.redirect('/dashboard/orders')}---
<Auth pageTitle="Edit Order"> <Row as="section" gap="xl" className="container"> <Col span={8}> <h1 class="text-3xl font-semibold mb-6">Edit Order</h1> <OrderFormWrapper orderId={orderId} loadingMessage="Loading order details..." errorMessages={{ notFound: "Order not found. It may have been deleted.", unauthorized: "You don't have permission to edit this order.", fetchFailed: "Failed to load order. Please try again." }} /> </Col> <Col span={4}> <OrderFormHelp /> </Col> </Row></Auth>Custom Authorization
Section titled “Custom Authorization”---import Auth from '#layouts/Auth.astro'import EventFormWrapper from '#components/astro/forms/EventFormWrapper.astro'
const eventId = Astro.params.id---
<Auth pageTitle="Edit Event" requiredRoles={['admin', 'event_manager']}> <section class="container"> <h1>Edit Event</h1> <EventFormWrapper eventId={eventId} requiredRoles={['admin', 'event_manager']} /> </section></Auth>Performance
Section titled “Performance”Server-Side Rendering (SSR)
Section titled “Server-Side Rendering (SSR)”- Fetch Time: 20-100ms (during SSR)
- Client Impact: Zero loading delay (data pre-fetched)
- User Experience: No loading spinners or layout shift
Request-Scoped Caching
Section titled “Request-Scoped Caching”- OrderFormWrapper: Clients list cached per request
- Benefit: N forms = 1 database query
- Duration: Request lifetime only (no stale data)
Code Size
Section titled “Code Size”- Before: 1,040 lines (8 pages × 130 lines)
- After: 960 lines (200 page lines + 600 wrapper lines + 160 help lines)
- Reduction: 8% smaller, 76% less duplication
Related Documentation
Section titled “Related Documentation”- Database Architecture - Database abstraction layer
- Authorization System - Role-based access control
- React Hook Form Pattern - Form validation with RHF + Zod
- Logging System - Structured logging
Summary
Section titled “Summary”Form wrapper components provide a powerful pattern for building consistent, maintainable CRUD forms with minimal code. By centralizing data fetching, authorization, error handling, and loading states, they enable you to focus on form UI and business logic.
Key Benefits:
- ✅ 76% code reduction in pages
- ✅ Consistent authorization and error handling
- ✅ Server-side rendering (no client loading states)
- ✅ Request-scoped caching for performance
- ✅ Flexible and extensible architecture