Skip to main content

DTO Design to Minimal Form Page

This guide details how to leverage Castlecraft Architect to accelerate frontend form development. By designing your Data Transfer Objects (DTOs) with UI-specific hints and using the architect generate forms command, you can automatically create form configurations. These configurations can then be integrated into your frontend to quickly scaffold functional, yet minimal, form pages.

While this process provides a functional starting point, it's crucial to understand that the scaffolded UI serves as a foundation and is not a substitute for dedicated UI/UX design. Optimal user experiences emerge from close collaboration between UI/UX designers and development teams on all aspects of the interface and data flow, including DTOs, API contracts (requests/responses), read models, and the underlying commands and queries.

Step 1: Designing the DTO (Python Pydantic Model)

This is the most crucial step. Your Pydantic models are the source of truth for both your API and, indirectly, your UI forms.

  1. Define Core Fields: Start by defining all the necessary fields for your DTO (e.g., CreateProductDTO, UpdateProductDTO). Include types, default values, and standard Pydantic validations (min_length, pattern, etc.).

  2. Add UI-Specific Metadata using json_schema_extra: For each field, use Field(..., json_schema_extra={...}) (Pydantic v2+) to add UI hints. These x-ui-* extensions will be picked up by architect generate forms.

    Key x-ui-* Extensions to Consider:

    • title (Standard Pydantic/OpenAPI): Used as the default field label.
    • description (Standard Pydantic/OpenAPI): Used as a base for placeholder or tooltip.
    • x-ui-order (integer): Specifies the rendering order of fields in the form.
    • x-ui-label (string): Overrides the title for the form field label if needed.
    • x-ui-placeholder (string): Specific placeholder text for the input.
    • x-ui-tooltip (string): Help text displayed as a tooltip next to the field.
    • x-ui-component (string): Suggests a specific UI component (e.g., "switch", "tags-input").
    • x-ui-multiline (boolean): Hints that a string field should be a textarea.
    • x-foreign-key-for (object): For dropdowns/selects populated from an API.
      • resourcePath (string): API endpoint to fetch options (e.g., "/categories").
      • optionValueKey (string): Field name from option object for value (e.g., "id").
      • optionLabelKey (string): Field name from option object for label (e.g., "name").
    • x-ui-read-only-on-create (boolean): Field is read-only on new resource creation.
    • x-ui-read-only-on-update (boolean): Field is read-only when editing.
    • x-ui-form-group (string): Name of the group for visual grouping.
    • x-ui-form-section / x-ui-tab (string): For organizing into larger sections or tabs.
    • x-ui-conditions (array of objects): Defines declarative conditional logic.
      • listenTo (string): Field name to monitor.
      • ifValue / ifExists / ifRegexMatches: Condition to check.
      • thenSet (object): Properties to apply if condition met (e.g., {"disabled": true}).
      • elseSet (object, optional): Properties if condition not met.
    • x-ui-on-change (object): Hints for dynamic actions on field change.
    • x-pattern-description (string): User-friendly description for a regex pattern.
    • x-generate-ui-form (boolean): Explicitly mark a DTO for form generation.
    • For Arrays of Objects (list-of-objects):
      • x-ui-formlist-item-schema (string): Name of schema defining item structure (if inline).
      • x-ui-formlist-fields (array of strings): Properties from item schema to render.

    Example DTO Field:

from pydantic import BaseModel, Field
from typing import Optional

class CreateProductDTO(BaseModel):
product_name: str = Field(
...,
title="Product Name",
description="The official name of the product.",
min_length=3,
json_schema_extra={
"x-ui-order": 1,
"x-ui-placeholder": "Enter product name (min 3 chars)",
"x-ui-form-group": "Basic Information"
}
)
category_id: Optional[int] = Field(
None,
title="Category",
json_schema_extra={
"x-ui-order": 2,
"x-ui-component": "SelectAsync",
"x-foreign-key-for": {
"resourcePath": "/categories",
"optionValueKey": "id",
"optionLabelKey": "name"
},
"x-ui-form-group": "Categorization"
}
)
is_active: bool = Field(
True,
title="Is Active",
json_schema_extra={
"x-ui-order": 3,
"x-ui-component": "switch",
"x-ui-form-group": "Status"
}
)
  1. Ensure DTO Naming Convention: For the --all flag in architect generate forms to pick up your DTOs automatically, ensure they follow the Create{Name}DTO or Update{Name}DTO pattern (e.g., CreateProductDTO, UpdateUserDTO).

Step 2: Generating Form Configurations

  1. Generate/Update openapi.json: Make sure your FastAPI backend is running and its /openapi.json endpoint is accessible and reflects the latest DTO changes. The architect generate forms command fetches this in memory.

  2. Execute architect generate forms: Open your terminal.

    • To generate for all detected DTOs: This will look for DTOs matching naming conventions (e.g., Create...DTO, Update...DTO) or those explicitly marked with x-generate-ui-form: true.
architect generate forms --all
  • To generate for specific DTOs:
architect generate forms --schemas CreateProductDTO UpdateProductDTO
  • Output: The script will create .config.js files (e.g., createProductForm.config.js) in your frontend/src/generated/ directory.

Step 3: Creating Your Minimal Boilerplate FormPage (Frontend)

Now, create a new React component for your resource's form (e.g., ProductFormPage.jsx).

  1. Import Necessary Modules:

    • React hooks (useMemo, useCallback).
    • Your generic ResourceFormPage component.
    • The newly generated configuration file.
    • API service functions.
    • Any custom functions for fetching related data.
  2. Define the Component:

// src/pages/ProductFormPage.jsx (Example)
import { useMemo, useCallback } from 'react';
import ResourceFormPage from '../components/generic/ResourceFormPage';
import { createProductBaseFormFieldsConfig } from '../generated/createProductForm.config.js';
import {
fetchResourceDetails, createResource, updateResource, deleteResource, fetchResourceList,
} from '../services/apiService';

const fetchCategories = async () => {
const result = await fetchResourceList('/categories', { page: 1, pageSize: 100 });
return result.items || [];
};

const ProductFormPage = () => {
const baseFormFieldsConfig = useMemo(() => createProductBaseFormFieldsConfig, []);

const apiFunctionsProp = useMemo(() => ({
fetchDetails: fetchResourceDetails, create: createResource, update: updateResource, deleteResource: deleteResource,
}), []);

const fetchRelatedDataFunctionsProp = useMemo(() => ({
categories: fetchCategories,
}), []);

const transformSubmitPayload = useCallback((values, _isCreating, _currentResourceData) => {
return values;
}, []);

const transformInitialValues = useCallback((data, isCreatingMode) => {
if (isCreatingMode) {
return { is_active: true, ...data };
}
return data;
}, []);

return (
<ResourceFormPage
resourceNameSingular="Product"
resourceNamePlural="Products"
resourceApiPath="/products"
idParamName="productId"
formFieldsConfig={baseFormFieldsConfig}
apiFunctions={apiFunctionsProp}
fetchRelatedDataFunctions={fetchRelatedDataFunctionsProp}
listRoutePath="/products"
transformInitialValues={transformInitialValues}
transformSubmitPayload={transformSubmitPayload}
itemKeyField="id"
/>
);
};

export default ProductFormPage;
  1. Add Routing: Ensure you have routes in your App.jsx (or your routing configuration file) that point to this new ProductFormPage for creating (/products/new) and editing (/products/:productId).

This process significantly reduces the manual effort in creating consistent and feature-rich forms across your application.