AI-ready development
Thunderclap Task Recipes
Thunderclap Task Recipes
Thunderclap generates a working CRUD module, but production features need customization. This page shows practical post-generation workflows with concrete before/after examples.
Recipe format
Each recipe follows this structure:
- Context: What was generated
- Task: What needs to change
- Files to modify: Exact paths
- Before: Generated code
- After: Customized code
- Verification: Commands to prove it works
Recipe 1: Add validation rules
Context
Thunderclap generated StoreProductRequest and UpdateProductRequest with basic validation.
Task
Add business rules:
name: required, max 255, uniquesku: required, max 50, unique, uppercaseprice: required, numeric, min 0.01description: nullable, max 1000
Files to modify
modules/Product/Requests/StoreProductRequest.phpmodules/Product/Requests/UpdateProductRequest.php
Before (generated)
// modules/Product/Requests/StoreProductRequest.phppublic function rules(): array{ return [ 'name' => ['required'], 'sku' => ['required'], 'price' => ['required'], 'description' => ['nullable'], ];}After (customized)
// modules/Product/Requests/StoreProductRequest.phppublic function rules(): array{ return [ 'name' => ['required', 'string', 'max:255', 'unique:products,name'], 'sku' => ['required', 'string', 'max:50', 'unique:products,sku', 'uppercase'], 'price' => ['required', 'numeric', 'min:0.01'], 'description' => ['nullable', 'string', 'max:1000'], ];}// modules/Product/Requests/UpdateProductRequest.phppublic function rules(): array{ return [ 'name' => ['required', 'string', 'max:255', 'unique:products,name,' . $this->route('product')->id], 'sku' => ['required', 'string', 'max:50', 'unique:products,sku,' . $this->route('product')->id, 'uppercase'], 'price' => ['required', 'numeric', 'min:0.01'], 'description' => ['nullable', 'string', 'max:1000'], ];}Verification
# Test validationphp artisan test --filter=ProductTest# Manual test: try creating product with duplicate SKU# Expected: validation error "The sku has already been taken."Recipe 2: Customize table columns
Context
Generated ProductTable shows all columns with basic text formatting.
Task
Customize listing:
- Show only: name, sku, price, is_active, created_at
- Format price as currency (IDR)
- Show is_active as badge (green/red)
- Make name, sku, price sortable
- Add search on name and sku
Files to modify
modules/Product/TableView.php
Before (generated)
use Laravolt\Suitable\Columns\Text;public function columns(): array{ return [ Text::make('id'), Text::make('name'), Text::make('sku'), Text::make('description'), Text::make('price'), Text::make('is_active'), Text::make('created_at'), Text::make('updated_at'), ];}After (customized)
use Laravolt\Suitable\Columns\Text;use Laravolt\Suitable\Columns\Boolean;use Laravolt\Suitable\Columns\DateTime;use Laravolt\Suitable\Columns\Number;public function columns(): array{ return [ Text::make('name', 'Product Name')->sortable(), Text::make('sku', 'SKU')->sortable(), Number::make('price', 'Price') ->sortable() ->prefix('Rp ') ->decimal(0), Boolean::make('is_active', 'Active'), DateTime::make('created_at', 'Created')->sortable(), ];}public function searchable(): array{ return ['name', 'sku'];}Verification
# Visit listing pagephp artisan serve# Open: http://localhost:8000/product# Check:# - Price shows "Rp 10,000" format# - Active shows green/red badge# - Search box filters by name/sku# - Column headers are clickable for sortRecipe 3: Add relationships
Context
Product belongs to Category. Category table exists.
Task
- Add
category_idforeign key to products table - Add relationship to Product model
- Show category in listing
- Add category select in form
Files to modify
- New migration
modules/Product/Models/Product.php(orapp/Models/Product.php)modules/Product/TableView.phpmodules/Product/resources/views/form.blade.phpmodules/Product/Requests/StoreProductRequest.phpmodules/Product/Requests/UpdateProductRequest.php
Step 1: Migration
php artisan make:migration add_category_id_to_products_tablepublic function up(){ Schema::table('products', function (Blueprint $table) { $table->foreignId('category_id')->nullable()->constrained()->cascadeOnDelete(); });}public function down(){ Schema::table('products', function (Blueprint $table) { $table->dropForeign(['category_id']); $table->dropColumn('category_id'); });}php artisan migrateStep 2: Add relationship to model
// modules/Product/Models/Product.phpuse App\Models\Category;protected $fillable = [ 'name', 'sku', 'description', 'price', 'is_active', 'category_id', // Add this];public function category(){ return $this->belongsTo(Category::class);}Step 3: Show category in table
// modules/Product/TableView.phpuse Laravolt\Suitable\Columns\Text;public function columns(): array{ return [ Text::make('name', 'Product Name')->sortable(), Text::make('sku', 'SKU')->sortable(), Text::make('category.name', 'Category'), // Add this // ... other columns ];}public function query(){ return Product::with('category'); // Eager load}Step 4: Add category select in form
// modules/Product/resources/views/form.blade.php{!! PrelineForm::text('name')->label('Product Name')->required() !!}{!! PrelineForm::text('sku')->label('SKU')->required() !!}{{-- Add this --}}{!! PrelineForm::select('category_id', $categories) ->label('Category') ->placeholder('-- Select Category --') ->required() !!}{!! PrelineForm::number('price')->label('Price')->required() !!}{!! PrelineForm::textarea('description')->label('Description')->rows(5) !!}{!! PrelineForm::checkbox('is_active', 1)->label('Active')->checked(old('is_active', $product->is_active ?? true)) !!}Step 5: Pass categories to view
// modules/Product/Controllers/ProductController.phppublic function create(){ $categories = \App\Models\Category::pluck('name', 'id'); return view('product::create', compact('categories'));}public function edit(Product $product){ $categories = \App\Models\Category::pluck('name', 'id'); return view('product::edit', compact('product', 'categories'));}Step 6: Update validation
// modules/Product/Requests/StoreProductRequest.phppublic function rules(): array{ return [ 'name' => ['required', 'string', 'max:255'], 'sku' => ['required', 'string', 'max:50', 'unique:products,sku'], 'category_id' => ['required', 'exists:categories,id'], // Add this 'price' => ['required', 'numeric', 'min:0.01'], 'description' => ['nullable', 'string', 'max:1000'], 'is_active' => ['boolean'], ];}Verification
# Run testsphp artisan test --filter=ProductTest# Check routesphp artisan route:list --name=product# Manual test:# 1. Visit /product/create# 2. Check category dropdown appears# 3. Try submitting without category (should fail validation)# 4. Create product with category# 5. Check listing shows category nameRecipe 4: Override generated views
Context
Generated form view uses default layout. Need custom styling.
Task
Wrap form fields in card, add section headers, use 2-column grid for name/SKU.
Files to modify
modules/Product/resources/views/form.blade.php
Before (generated)
{!! PrelineForm::text('name')->label('Product Name')->required() !!}{!! PrelineForm::text('sku')->label('SKU')->required() !!}{!! PrelineForm::number('price')->label('Price')->required() !!}{!! PrelineForm::textarea('description')->label('Description') !!}{!! PrelineForm::checkbox('is_active', 1)->label('Active') !!}After (customized)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-6"> {{-- Basic Information --}} <div> <h3 class="text-lg font-semibold mb-4">Basic Information</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> {!! PrelineForm::text('name')->label('Product Name')->required() !!} {!! PrelineForm::text('sku') ->label('SKU') ->required() ->help('Format: PROD-XXXX') !!} </div> </div> {{-- Pricing --}} <div> <h3 class="text-lg font-semibold mb-4">Pricing</h3> {!! PrelineForm::number('price') ->label('Price (IDR)') ->required() ->min(0) ->step(0.01) !!} </div> {{-- Details --}} <div> <h3 class="text-lg font-semibold mb-4">Details</h3> {!! PrelineForm::textarea('description') ->label('Description') ->rows(5) ->help('Maximum 1000 characters') !!} </div> {{-- Status --}} <div> <h3 class="text-lg font-semibold mb-4">Status</h3> {!! PrelineForm::checkbox('is_active', 1) ->label('Active') ->checked(old('is_active', $product->is_active ?? true)) !!} </div></div>Verification
# Visit create/edit pages# Check:# - Form is wrapped in card# - Sections have headers# - Name and SKU are side-by-side on desktop# - Help text appears under fieldsRecipe 5: Add custom actions
Context
Need "Duplicate" action on product listing.
Task
Add "Duplicate" button to table row actions that copies product with new SKU.
Files to modify
modules/Product/TableView.phpmodules/Product/Controllers/ProductController.phpmodules/Product/routes/web.php
Step 1: Add route
// modules/Product/routes/web.phpRoute::post('product/{product}/duplicate', [ProductController::class, 'duplicate']) ->name('product.duplicate');Step 2: Add controller method
// modules/Product/Controllers/ProductController.phppublic function duplicate(Product $product){ $this->authorize('create', Product::class); $newProduct = $product->replicate(); $newProduct->sku = $product->sku . '-COPY-' . time(); $newProduct->name = $product->name . ' (Copy)'; $newProduct->save(); return redirect() ->route('product.index') ->with('success', 'Product duplicated successfully.');}Step 3: Add action to table
// modules/Product/TableView.phpuse Laravolt\Suitable\Columns\Action;public function columns(): array{ return [ // ... other columns Action::make('actions') ->label('Actions') ->actions(function ($row) { return [ [ 'label' => 'View', 'url' => route('product.show', $row), ], [ 'label' => 'Edit', 'url' => route('product.edit', $row), ], [ 'label' => 'Duplicate', 'url' => route('product.duplicate', $row), 'method' => 'POST', 'confirm' => 'Duplicate this product?', ], [ 'label' => 'Delete', 'url' => route('product.destroy', $row), 'method' => 'DELETE', 'confirm' => 'Delete this product?', ], ]; }), ];}Verification
# Check route existsphp artisan route:list --name=product.duplicate# Manual test:# 1. Visit /product# 2. Click "Duplicate" on any product# 3. Confirm dialog appears# 4. Check new product created with "-COPY-" suffixCommon pitfalls
1. Forgetting to update both Store and Update requests
Problem: Added validation to StoreProductRequest but not UpdateProductRequest.
Fix: Always update both request classes with the same rules (except unique constraints).
2. Not eager loading relationships
Problem: N+1 query when showing category in product listing.
Fix: Add with('category') to table query method.
3. Missing authorization checks
Problem: Generated controller has no policy checks.
Fix: Add policy and $this->authorize() calls in controller methods.
4. Hardcoded values in views
Problem: Form has hardcoded category options.
Fix: Pass dynamic data from controller to view.
AI-friendly task templates
Use these templates when prompting AI agents:
Template: Add validation
Update [Module] validation:- [field]: [rules]- [field]: [rules]Files: modules/[Module]/Requests/Store[Module]Request.php, Update[Module]Request.phpReturn: updated rules, test commandTemplate: Customize table
Update [Module] table:- Show columns: [list]- Format [column] as [type]- Make [columns] sortable- Add search on [columns]File: modules/[Module]/TableView.phpReturn: updated columns() methodTemplate: Add relationship
Add [relationship] to [Module]:1. Migration: add [foreign_key] to [table]2. Model: add [relationship]() method3. Table: show [related_field]4. Form: add [select/input] for [relationship]5. Validation: add [foreign_key] rulesReturn: migration, model, table, form, validation changesWhat to read next
- Thunderclap — core scaffolding concepts
- Forms overview — PrelineForm API reference
- Tables and listings — Suitable API reference
- AI task patterns — prompt templates for common tasks