- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
29 KiB
LiveComponents End-to-End Creation Guide
Complete guide for creating LiveComponents from scratch - from concept to production deployment.
Table of Contents
- Overview
- Quick Start
- Step-by-Step Tutorial
- Component Architecture
- Advanced Patterns
- Testing
- Deployment
- Troubleshooting
Overview
A LiveComponent is a server-rendered, interactive UI component with automatic state synchronization. Components are:
- Server-rendered: HTML generated on the server
- Stateful: Maintain state across interactions
- Interactive: Respond to user actions without full page reloads
- Framework-compliant: Readonly classes, Value Objects, Composition
When to Use LiveComponents
✅ Use LiveComponents for:
- Interactive forms with validation
- Real-time dashboards
- Shopping carts
- Search interfaces with filters
- File uploads with progress
- Paginated lists
- Wizards/multi-step forms
❌ Don't use LiveComponents for:
- Static content (use regular templates)
- Simple links (use standard HTML)
- High-frequency updates (use WebSockets directly)
- Complex client-side logic (use dedicated JavaScript)
Quick Start
Minimal Component (5 minutes)
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Counter;
use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
// 1. Mark class with #[LiveComponent] attribute
#[LiveComponent('counter')]
// 2. Implement LiveComponentContract
final readonly class CounterComponent implements LiveComponentContract
{
// 3. Constructor: ComponentId + State
public function __construct(
public ComponentId $id,
public CounterState $state
) {}
// 4. Define actions with #[Action]
#[Action]
public function increment(): CounterState
{
return $this->state->increment();
}
#[Action]
public function decrement(): CounterState
{
return $this->state->decrement();
}
// 5. Render method returns template path
public function render(): string
{
return 'components/counter';
}
}
State Value Object
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Counter;
use App\Framework\LiveComponents\Contracts\ComponentData;
use DateTimeImmutable;
// State must implement ComponentData
final readonly class CounterState implements ComponentData
{
public function __construct(
public int $count = 0,
public ?DateTimeImmutable $lastUpdate = null
) {}
// Immutable state updates
public function increment(): self
{
return new self(
count: $this->count + 1,
lastUpdate: new DateTimeImmutable()
);
}
public function decrement(): self
{
return new self(
count: max(0, $this->count - 1), // Prevent negative
lastUpdate: new DateTimeImmutable()
);
}
// Required by ComponentData
public function toArray(): array
{
return [
'count' => $this->count,
'lastUpdate' => $this->lastUpdate?->format('Y-m-d H:i:s')
];
}
}
Template
<!-- resources/views/components/counter.view.php -->
<div data-lc-component="counter" data-lc-id="{component_id}">
<h2>Counter: {count}</h2>
<button data-lc-action="increment">+1</button>
<button data-lc-action="decrement">-1</button>
<if condition="lastUpdate">
<p>Last update: {lastUpdate}</p>
</if>
</div>
Usage in Controller
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\View\ViewResult;
public function show(ComponentRegistry $registry): ViewResult
{
$component = $registry->resolve(
ComponentId::fromString('counter:main'),
initialData: ['count' => 0]
);
return new ViewResult('pages/counter', [
'component' => $component
]);
}
That's it! Component is now interactive. Actions automatically sync state to the server.
Step-by-Step Tutorial
Let's build a Shopping Cart Component from scratch.
Step 1: Define the Domain
<?php
// src/Application/LiveComponents/ShoppingCart/CartItem.php
namespace App\Application\LiveComponents\ShoppingCart;
use App\Framework\Core\ValueObjects\Price;
final readonly class CartItem
{
public function __construct(
public string $productId,
public string $name,
public Price $price,
public int $quantity
) {
if ($quantity < 1) {
throw new \InvalidArgumentException('Quantity must be at least 1');
}
}
public function getTotal(): Price
{
return $this->price->multiply($this->quantity);
}
public function withQuantity(int $quantity): self
{
return new self(
$this->productId,
$this->name,
$this->price,
$quantity
);
}
public function toArray(): array
{
return [
'product_id' => $this->productId,
'name' => $this->name,
'price' => $this->price->toDecimal(),
'quantity' => $this->quantity,
'total' => $this->getTotal()->toDecimal()
];
}
}
Step 2: Create State Value Object
<?php
// src/Application/LiveComponents/ShoppingCart/ShoppingCartState.php
namespace App\Application\LiveComponents\ShoppingCart;
use App\Framework\LiveComponents\Contracts\ComponentData;
use App\Framework\Core\ValueObjects\Price;
final readonly class ShoppingCartState implements ComponentData
{
/** @param array<CartItem> $items */
public function __construct(
public array $items = [],
public ?string $couponCode = null,
public int $discountPercentage = 0
) {}
public function addItem(CartItem $item): self
{
// Check if item already exists
foreach ($this->items as $existingItem) {
if ($existingItem->productId === $item->productId) {
// Update quantity
return $this->updateQuantity(
$item->productId,
$existingItem->quantity + $item->quantity
);
}
}
// Add new item
return new self(
items: [...$this->items, $item],
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function removeItem(string $productId): self
{
return new self(
items: array_values(array_filter(
$this->items,
fn(CartItem $item) => $item->productId !== $productId
)),
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function updateQuantity(string $productId, int $quantity): self
{
if ($quantity < 1) {
return $this->removeItem($productId);
}
$updatedItems = array_map(
fn(CartItem $item) => $item->productId === $productId
? $item->withQuantity($quantity)
: $item,
$this->items
);
return new self(
items: $updatedItems,
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function applyCoupon(string $couponCode, int $discountPercentage): self
{
return new self(
items: $this->items,
couponCode: $couponCode,
discountPercentage: $discountPercentage
);
}
public function getSubtotal(): Price
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getTotal()->cents;
}
return Price::fromCents($total);
}
public function getDiscount(): Price
{
if ($this->discountPercentage === 0) {
return Price::fromCents(0);
}
$subtotal = $this->getSubtotal();
$discountAmount = ($subtotal->cents * $this->discountPercentage) / 100;
return Price::fromCents((int) $discountAmount);
}
public function getTotal(): Price
{
return $this->getSubtotal()->subtract($this->getDiscount());
}
public function getItemCount(): int
{
return array_reduce(
$this->items,
fn(int $sum, CartItem $item) => $sum + $item->quantity,
0
);
}
public function isEmpty(): bool
{
return empty($this->items);
}
public function toArray(): array
{
return [
'items' => array_map(fn(CartItem $item) => $item->toArray(), $this->items),
'coupon_code' => $this->couponCode,
'discount_percentage' => $this->discountPercentage,
'subtotal' => $this->getSubtotal()->toDecimal(),
'discount' => $this->getDiscount()->toDecimal(),
'total' => $this->getTotal()->toDecimal(),
'item_count' => $this->getItemCount(),
'is_empty' => $this->isEmpty()
];
}
}
Step 3: Create Component
<?php
// src/Application/LiveComponents/ShoppingCart/ShoppingCartComponent.php
namespace App\Application\LiveComponents\ShoppingCart;
use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentEventDispatcher;
use App\Framework\LiveComponents\ValueObjects\EventPayload;
use App\Framework\Core\ValueObjects\Price;
#[LiveComponent('shopping-cart')]
final readonly class ShoppingCartComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public ShoppingCartState $state,
private CouponService $couponService // Injected service
) {}
#[Action(rateLimit: 30, rateLimitWindow: 60)] // 30 updates per minute
public function addItem(
string $product_id,
string $name,
int $price_cents,
int $quantity = 1,
?ComponentEventDispatcher $events = null
): ShoppingCartState {
$item = new CartItem(
productId: $product_id,
name: $name,
price: Price::fromCents($price_cents),
quantity: $quantity
);
$newState = $this->state->addItem($item);
// Dispatch event for analytics
$events?->dispatch('cart:item-added', EventPayload::fromArray([
'product_id' => $product_id,
'quantity' => $quantity,
'total_items' => $newState->getItemCount()
]));
return $newState;
}
#[Action]
public function removeItem(
string $product_id,
?ComponentEventDispatcher $events = null
): ShoppingCartState {
$newState = $this->state->removeItem($product_id);
$events?->dispatch('cart:item-removed', EventPayload::fromArray([
'product_id' => $product_id,
'total_items' => $newState->getItemCount()
]));
return $newState;
}
#[Action]
public function updateQuantity(
string $product_id,
int $quantity
): ShoppingCartState {
return $this->state->updateQuantity($product_id, $quantity);
}
#[Action(rateLimit: 5, rateLimitWindow: 300)] // 5 coupon attempts per 5 min
public function applyCoupon(
string $coupon_code,
?ComponentEventDispatcher $events = null
): ShoppingCartState {
// Validate coupon via service
$coupon = $this->couponService->validate($coupon_code);
if (!$coupon->isValid()) {
$events?->dispatch('cart:coupon-invalid', EventPayload::fromArray([
'coupon_code' => $coupon_code,
'reason' => $coupon->getInvalidReason()
]));
// Return current state unchanged
return $this->state;
}
$newState = $this->state->applyCoupon(
$coupon_code,
$coupon->getDiscountPercentage()
);
$events?->dispatch('cart:coupon-applied', EventPayload::fromArray([
'coupon_code' => $coupon_code,
'discount_percentage' => $coupon->getDiscountPercentage(),
'discount_amount' => $newState->getDiscount()->toDecimal()
]));
return $newState;
}
#[Action(
rateLimit: 3,
rateLimitWindow: 300,
idempotencyTTL: 3600
)]
public function checkout(?ComponentEventDispatcher $events = null): ShoppingCartState
{
if ($this->state->isEmpty()) {
$events?->dispatch('cart:checkout-failed', EventPayload::fromArray([
'reason' => 'empty_cart'
]));
return $this->state;
}
// Process checkout (would integrate with payment service)
$events?->dispatch('cart:checkout-initiated', EventPayload::fromArray([
'total' => $this->state->getTotal()->toDecimal(),
'item_count' => $this->state->getItemCount()
]));
return $this->state;
}
public function render(): string
{
return 'components/shopping-cart';
}
}
Step 4: Create Template
<!-- resources/views/components/shopping-cart.view.php -->
<div
data-lc-component="shopping-cart"
data-lc-id="{component_id}"
class="shopping-cart"
>
<h2>Shopping Cart ({item_count} items)</h2>
<if condition="is_empty">
<p class="empty-message">Your cart is empty</p>
</if>
<if condition="!is_empty">
<!-- Cart Items (Fragment for partial updates) -->
<div data-lc-fragment="cart-items">
<for items="items" as="item">
<div class="cart-item">
<h3>{item.name}</h3>
<p>Price: €{item.price}</p>
<div class="quantity-control">
<button
data-lc-action="updateQuantity"
data-lc-params='{"product_id": "{item.product_id}", "quantity": {item.quantity - 1}}'
>-</button>
<span>{item.quantity}</span>
<button
data-lc-action="updateQuantity"
data-lc-params='{"product_id": "{item.product_id}", "quantity": {item.quantity + 1}}'
>+</button>
</div>
<p>Total: €{item.total}</p>
<button
data-lc-action="removeItem"
data-lc-params='{"product_id": "{item.product_id}"}'
class="remove-btn"
>Remove</button>
</div>
</for>
</div>
<!-- Cart Summary (Fragment for partial updates) -->
<div data-lc-fragment="cart-summary" class="cart-summary">
<div>
<span>Subtotal:</span>
<span>€{subtotal}</span>
</div>
<if condition="discount_percentage > 0">
<div class="discount">
<span>Discount ({discount_percentage}%):</span>
<span>-€{discount}</span>
</div>
</if>
<div class="total">
<span>Total:</span>
<span>€{total}</span>
</div>
</div>
<!-- Coupon Form -->
<form data-lc-action="applyCoupon" class="coupon-form">
<input type="text" name="coupon_code" placeholder="Coupon code" />
<button type="submit">Apply</button>
</form>
<!-- Checkout Button -->
<button
data-lc-action="checkout"
class="checkout-btn"
data-lc-confirm="Proceed to checkout?"
>
Checkout (€{total})
</button>
</if>
</div>
Step 5: JavaScript Integration (Optional)
// resources/js/modules/shopping-cart.js
import { LiveComponent } from './live-component.js';
const cartComponent = new LiveComponent('shopping-cart:main');
// Listen to events
cartComponent.on('cart:item-added', (data) => {
console.log('Item added:', data.product_id);
// Show notification
showNotification(`Added ${data.quantity} item(s) to cart`);
// Update cart badge
updateCartBadge(data.total_items);
});
cartComponent.on('cart:coupon-applied', (data) => {
showNotification(
`Coupon applied! You save ${data.discount_percentage}%`,
'success'
);
});
cartComponent.on('cart:coupon-invalid', (data) => {
showNotification(
`Invalid coupon: ${data.reason}`,
'error'
);
});
cartComponent.on('cart:checkout-initiated', (data) => {
// Redirect to checkout page
window.location.href = `/checkout?total=${data.total}`;
});
// Fragment updates for better performance
cartComponent.on('action:updateQuantity', async (event) => {
// Only update cart-items and cart-summary fragments
await cartComponent.executeAction(
'updateQuantity',
event.params,
{ fragments: ['cart-items', 'cart-summary'] }
);
});
Step 6: Testing
<?php
// tests/Feature/LiveComponents/ShoppingCartComponentTest.php
use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;
describe('ShoppingCartComponent', function () {
it('starts with empty cart', function () {
$component = mountComponent('shopping-cart:test');
expect($component['state'])->toHaveStateKey('is_empty', true);
expect($component['state'])->toHaveStateKey('item_count', 0);
});
it('adds items to cart', function () {
$component = mountComponent('shopping-cart:test');
$result = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 1999,
'quantity' => 2
]);
expect($result['state'])->toHaveStateKey('item_count', 2);
expect($result['state'])->toHaveStateKey('subtotal', '39.98');
expect($result['events'])->toHaveDispatchedEvent('cart:item-added');
});
it('applies valid coupon', function () {
$component = mountComponent('shopping-cart:test');
// Add item first
$component = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 10000,
'quantity' => 1
]);
// Apply coupon
$result = callAction($component, 'applyCoupon', [
'coupon_code' => 'SAVE20'
]);
expect($result['state'])->toHaveStateKey('discount_percentage', 20);
expect($result['state'])->toHaveStateKey('discount', '20.00');
expect($result['state'])->toHaveStateKey('total', '80.00');
expect($result['events'])->toHaveDispatchedEvent('cart:coupon-applied');
});
it('enforces rate limiting on checkout', function () {
$component = mountComponent('shopping-cart:test');
// Add item
$component = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 10000
]);
// Try to checkout 4 times (limit is 3)
callAction($component, 'checkout');
callAction($component, 'checkout');
callAction($component, 'checkout');
// 4th attempt should be rate limited
expect(fn() => callAction($component, 'checkout'))
->toThrow(RateLimitExceededException::class);
});
});
Component Architecture
Required Elements
Every LiveComponent must have:
- #[LiveComponent] attribute with unique name
- LiveComponentContract implementation
- ComponentId + State in constructor
- State as ComponentData Value Object
- render() method returning template path
Optional Elements
- #[Action] methods for interactivity
- Service injection via constructor
- Event dispatching for side effects
- Pollable interface for auto-refresh
- Fragment markers for partial updates
Component Lifecycle
1. Resolve (ComponentRegistry)
├─ Create instance via DI Container
├─ Inject ComponentId
├─ Inject initial State
└─ Inject Services
2. Render (LiveComponentRenderer)
├─ Call render() for template path
├─ Pass state data to template
├─ Process template (placeholders, conditionals, loops)
└─ Return HTML
3. Action Execution (LiveComponentHandler)
├─ Validate CSRF token
├─ Check rate limits
├─ Check idempotency
├─ Bind parameters
├─ Execute action method
├─ Create new State from return value
├─ Dispatch events
└─ Return ComponentUpdate
4. Re-render
├─ Create new component instance with new State
├─ Render updated HTML
├─ Extract fragments (if requested)
└─ Return response (html/fragments + state + events)
Advanced Patterns
Service Injection
#[LiveComponent('user-profile')]
final readonly class UserProfileComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public UserProfileState $state,
// Inject services via constructor
private UserRepository $userRepository,
private AvatarService $avatarService,
private PermissionChecker $permissions
) {}
#[Action]
public function updateAvatar(UploadedFile $avatar): UserProfileState
{
$url = $this->avatarService->upload($avatar);
return $this->state->withAvatarUrl($url);
}
}
Event Dispatching
#[Action]
public function completeOrder(
?ComponentEventDispatcher $events = null
): OrderState {
$order = $this->orderService->complete($this->state->orderId);
// Dispatch domain event
$events?->dispatch('order:completed', EventPayload::fromArray([
'order_id' => $order->id,
'total' => $order->total->toDecimal(),
'customer_id' => $order->customerId
]));
// Other systems can listen to this event
// - Send confirmation email
// - Update inventory
// - Notify warehouse
// - Track analytics
return $this->state->withOrder($order);
}
Polling for Real-Time Updates
use App\Framework\LiveComponents\Contracts\Pollable;
#[LiveComponent('order-status')]
final readonly class OrderStatusComponent implements
LiveComponentContract,
Pollable
{
#[Action]
public function poll(): OrderStatusState
{
// Fetch latest order status
$order = $this->orderRepository->find($this->state->orderId);
return $this->state->withStatus($order->status);
}
public function getPollInterval(): int
{
return 5000; // Poll every 5 seconds
}
}
Fragment-Based Updates
// Template with fragments
<div data-lc-component="product-list">
<!-- Only update this part -->
<div data-lc-fragment="product-grid">
<for items="products" as="product">
<!-- Product cards -->
</for>
</div>
<!-- Only update this part -->
<div data-lc-fragment="pagination">
<!-- Pagination controls -->
</div>
</div>
// JavaScript
liveComponent.executeAction('changePage', { page: 2 }, {
fragments: ['product-grid', 'pagination'] // Only update these
});
DTOs for Complex Parameters
// Define DTO
final readonly class UpdateProfileRequest
{
public function __construct(
public string $name,
public Email $email,
public ?string $bio = null,
public bool $newsletter = false
) {}
}
// Use in action
#[Action]
public function updateProfile(UpdateProfileRequest $request): UserProfileState
{
// $request is automatically instantiated from action parameters
return $this->state->update(
name: $request->name,
email: $request->email,
bio: $request->bio,
newsletter: $request->newsletter
);
}
Testing
See tests/Feature/LiveComponents/CounterComponentTest.php for examples.
Testing Helpers
use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;
use function Pest\LiveComponents\callActionWithFragments;
// Mount component
$component = mountComponent('counter:test', ['count' => 0]);
// Execute action
$result = callAction($component, 'increment');
// Execute action with fragments
$result = callActionWithFragments(
$component,
'updateGrid',
params: ['page' => 2],
fragments: ['product-grid', 'pagination']
);
Custom Expectations
// HTML assertions
expect($component['html'])->toContainHtml('Count: 5');
// State assertions
expect($component['state'])->toHaveState(['count' => 5]);
expect($component['state'])->toHaveStateKey('count', 5);
// Event assertions
expect($result['events'])->toHaveDispatchedEvent('counter:changed');
expect($result['events'])->toHaveDispatchedEventWithData('counter:changed', [
'old_value' => 4,
'new_value' => 5
]);
// Fragment assertions
expect($result['html'])->toHaveFragment('cart-summary', '€99.99');
Deployment
Production Checklist
- All actions have
#[Action]attribute - Rate limits configured for critical actions
- Idempotency TTL set for state-changing actions
- CSRF protection enabled (default)
- Events logged for analytics
- Error handling implemented
- Tests passing
- Templates optimized (fragments for large updates)
- JavaScript client configured
- Monitoring/alerting set up
Performance Tips
- Use fragments for partial updates
- Configure polling interval based on data freshness needs
- Add caching via
#[Action(cache: true)] - Optimize templates - minimize DOM size
- Lazy load components not immediately visible
Security Tips
- Never trust client input - validate in actions
- Use DTOs for complex parameters
- Set rate limits on all actions
- Enable idempotency for critical operations
- Log security events for monitoring
Troubleshooting
Component Not Found
Error: "Component 'my-component' not found"
Causes:
- Missing
#[LiveComponent]attribute - Typo in component name
- Component class not in
Application/LiveComponents/directory - Discovery cache stale
Solutions:
# Clear discovery cache
php console.php discovery:clear
# Verify component is registered
php console.php mcp:server
# Then use analyze_codebase to find LiveComponents
Action Not Callable
Error: "Action 'myAction' not found"
Causes:
- Missing
#[Action]attribute - Method is private/protected
- Method is static
- Reserved method name (render, toArray, etc.)
Solutions:
// ✅ Correct
#[Action]
public function myAction(): StateType
// ❌ Wrong - missing attribute
public function myAction(): StateType
// ❌ Wrong - static
#[Action]
public static function myAction(): StateType
// ❌ Wrong - private
#[Action]
private function myAction(): StateType
State Not Updating
Causes:
- Action not returning new state
- State mutation instead of new instance
- Caching issues
Solutions:
// ❌ Wrong - mutation (doesn't work with readonly)
public function increment(): void
{
$this->state->count++; // Can't mutate readonly
}
// ✅ Correct - return new state
public function increment(): CounterState
{
return $this->state->increment(); // Returns new instance
}
Events Not Dispatching
Causes:
- Missing
ComponentEventDispatcherparameter - Not calling
dispatch()method - Events not handled in JavaScript
Solutions:
// ✅ Correct
#[Action]
public function save(?ComponentEventDispatcher $events = null): StateType
{
$events?->dispatch('saved', EventPayload::fromArray(['id' => 123]));
return $newState;
}
// JavaScript
liveComponent.on('saved', (data) => {
console.log('Saved:', data.id);
});
Next Steps
- Read Security Guide for CSRF/Rate Limiting/Idempotency
- Read Performance Guide for optimization strategies
- Read API Reference for complete attribute/contract documentation
- Explore example components in
src/Application/LiveComponents/ - Run tests:
./vendor/bin/pest tests/Feature/LiveComponents/
Summary
LiveComponents provide:
✅ Server-rendered interactivity without JavaScript complexity ✅ Type-safe state management with Value Objects ✅ Built-in security (CSRF, Rate Limiting, Idempotency) ✅ Framework-compliant (Readonly, Immutable, Composition) ✅ Easy testing with Pest helpers ✅ Production-ready with monitoring and error handling
Start simple with a counter, then build more complex components as you learn the patterns.