Documentation Index
Fetch the complete documentation index at: https://docs.synapsebuilder.org/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Follow these best practices to create high-quality, performant, and maintainable Shopify extensions with Synapse.
Bundle Size
Keep extensions under 50KB
- Remove unused imports
- Avoid large dependencies
- Use tree-shaking
- Code splitting
Load Time
Load in under 500ms
- Lazy load components
- Minimize initial render
- Cache API responses
- Optimize images
Re-renders
Minimize unnecessary renders
- Use React.memo
- Optimize useEffect deps
- Memoize calculations
- Avoid inline functions
Function Speed
Functions under 5ms
- Use Rust over JavaScript
- Minimize loops
- Cache results
- Limit query surface
Code Quality
TypeScript Best Practices
Strict Types
Type Guards
Interface Definitions
Use strict TypeScript configuration:// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Benefits:
- Catch errors early
- Better IDE support
- Self-documenting code
- Easier refactoring
Handle undefined values safely:// ❌ Bad: Assumes value exists
const country = address.country;
// ✅ Good: Type guard
const country = address?.country ?? 'US';
// ✅ Good: Conditional rendering
if (!address) return null;
const country = address.country;
// ✅ Good: Type narrowing
function isProductVariant(
item: any
): item is ProductVariant {
return item.__typename === 'ProductVariant';
}
Define clear interfaces:interface DeliveryEstimate {
startDate: string;
endDate: string;
confidence: 'high' | 'medium' | 'low';
method: string;
}
interface ExtensionProps {
address: Address;
cartTotal: number;
onEstimateCalculated?: (estimate: DeliveryEstimate) => void;
}
function Extension({
address,
cartTotal,
onEstimateCalculated
}: ExtensionProps) {
// Implementation
}
Component Structure
Single Responsibility
Each component does one thing:// ❌ Bad: Component does too much
function DeliveryEstimate() {
const address = useShippingAddress();
const lines = useCartLines();
const [estimate, setEstimate] = useState(null);
useEffect(() => {
// Complex calculation
// API calls
// Formatting
}, [address, lines]);
return (
<View>
{/* Complex rendering */}
</View>
);
}
// ✅ Good: Separated concerns
function DeliveryEstimate() {
const estimate = useDeliveryEstimate();
return <DeliveryDisplay estimate={estimate} />;
}
function useDeliveryEstimate() {
// Custom hook handles logic
}
function DeliveryDisplay({ estimate }) {
// Component handles rendering
}
Extract Custom Hooks
Reuse logic with custom hooks:// hooks/useDeliveryEstimate.ts
export function useDeliveryEstimate() {
const address = useShippingAddress();
const [estimate, setEstimate] = useState<DeliveryEstimate | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!address) return;
setLoading(true);
calculateDeliveryDate(address)
.then(setEstimate)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [address]);
return { estimate, loading, error };
}
// Usage
function Extension() {
const { estimate, loading, error } = useDeliveryEstimate();
if (loading) return <SkeletonText />;
if (error) return <ErrorBanner message={error} />;
if (!estimate) return null;
return <DeliveryDisplay estimate={estimate} />;
}
Memoize Expensive Operations
Use useMemo for calculations:function Extension() {
const lines = useCartLines();
// ❌ Bad: Recalculates every render
const total = lines.reduce((sum, line) =>
sum + parseFloat(line.cost.totalAmount.amount), 0
);
// ✅ Good: Only recalculates when lines change
const total = useMemo(() =>
lines.reduce((sum, line) =>
sum + parseFloat(line.cost.totalAmount.amount), 0
),
[lines]
);
return <Total amount={total} />;
}
Function Optimization
Rust vs JavaScript
When to Use Rust
When to Use JavaScript
Performance Tips
Use Rust for:
- Production functions
- Performance-critical logic
- Complex calculations
- Large-scale operations
Benefits:
- 10-100x faster than JS
- Compiled to WebAssembly
- Type safety
- No garbage collection
Example:// Fast execution, < 1ms typical
fn calculate_discount(lines: &[CartLine]) -> Vec<Discount> {
lines.iter()
.filter_map(|line| {
if line.quantity >= 10 {
Some(create_discount(line, 20.0))
} else {
None
}
})
.collect()
}
Use JavaScript for:
- Rapid prototyping
- Simple logic
- Development/testing
- Easy modifications
Benefits:
- Easier to modify
- Faster iteration
- No compilation
- Familiar syntax
Example:// Simple logic, easy to modify
export function run(input) {
return {
discounts: input.cart.lines
.filter(line => line.quantity >= 10)
.map(line => createDiscount(line, 20))
};
}
Optimization techniques:// ✅ Good: Single pass
let discounts: Vec<_> = lines.iter()
.filter_map(|line| calculate_discount(line))
.collect();
// ❌ Bad: Multiple passes
let eligible: Vec<_> = lines.iter()
.filter(|line| is_eligible(line))
.collect();
let discounts: Vec<_> = eligible.iter()
.map(|line| calculate_discount(line))
.collect();
// ✅ Good: Early return
if input.cart.lines.is_empty() {
return Ok(FunctionRunResult {
operations: vec![]
});
}
// ✅ Good: Avoid cloning
fn process_line(line: &CartLine) -> Option<Discount> {
// Use references, avoid clones
}
Error Handling
Handle failures without breaking:function Extension() {
const { data, error } = useQuery();
// ❌ Bad: Throws error
if (error) throw error;
// ✅ Good: Shows fallback
if (error) {
return (
<Banner status="warning">
Unable to load delivery estimate.
Standard shipping applies.
</Banner>
);
}
// ✅ Good: Degrades gracefully
if (!data) {
return <SkeletonText />; // Loading state
}
return <Content data={data} />;
}
Catch component errors:import { ErrorBoundary } from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.block.render',
() => (
<ErrorBoundary
onError={(error) => {
console.error('Extension error:', error);
// Log to monitoring service
}}
>
<Extension />
</ErrorBoundary>
)
);
Never panic in functions:// ❌ Bad: Panics on error
let quantity = line.quantity.parse::<i64>().unwrap();
// ✅ Good: Returns default
let quantity = line.quantity.parse::<i64>().unwrap_or(0);
// ✅ Good: Returns empty on error
fn run(input: Input) -> Result<FunctionRunResult> {
let operations = match calculate_operations(&input) {
Ok(ops) => ops,
Err(e) => {
eprintln!("Error: {}", e);
vec![] // Return empty, don't crash
}
};
Ok(FunctionRunResult { operations })
}
Explain errors clearly:// ❌ Bad: Technical jargon
<Banner status="critical">
GraphQL query failed: Field 'deliveryAddress' not found
</Banner>
// ✅ Good: User-friendly
<Banner status="info">
Unable to calculate delivery estimate.
Please enter your shipping address.
</Banner>
// ✅ Good: Actionable
<Banner status="warning">
Delivery estimate temporarily unavailable.
<Button onPress={retry}>Try Again</Button>
</Banner>
Testing
Unit Tests
Integration Tests
Function Tests
Test individual functions:// utils.test.ts
import { describe, it, expect } from 'vitest';
import { calculateDeliveryDate } from './utils';
describe('calculateDeliveryDate', () => {
it('adds business days correctly', () => {
const result = calculateDeliveryDate({
country: 'US',
province: 'CA'
});
expect(result.startDate).toBeDefined();
expect(result.endDate).toBeDefined();
expect(result.confidence).toBe('high');
});
it('handles invalid addresses', () => {
const result = calculateDeliveryDate({
country: 'XX'
});
expect(result.error).toBe('Invalid country');
});
it('accounts for weekends', () => {
const result = calculateDeliveryDate({
country: 'US'
}, new Date('2025-10-31')); // Friday
// Should skip weekend
expect(result.startDate).not.toBe('2025-11-01');
});
});
Test extension rendering:// Extension.test.tsx
import { render, screen } from '@testing-library/react';
import { Extension } from './Extension';
describe('Extension', () => {
it('renders delivery estimate', () => {
render(<Extension address={mockAddress} />);
expect(screen.getByText(/estimated delivery/i)).toBeInTheDocument();
});
it('shows loading state', () => {
render(<Extension address={null} />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('handles errors gracefully', async () => {
mockFetch.mockRejectedValueOnce(new Error('API error'));
render(<Extension address={mockAddress} />);
expect(await screen.findByText(/unable to calculate/i))
.toBeInTheDocument();
});
});
Test Shopify Functions:# Create test input
cat > test-input.json << EOF
{
"cart": {
"lines": [
{
"quantity": 5,
"merchandise": {
"id": "gid://shopify/ProductVariant/123"
}
}
]
}
}
EOF
# Test function
cargo run < test-input.json > output.json
# Verify output
cat output.json | jq '.discounts | length'
# Should be > 0 for quantity >= 5
Accessibility
Keyboard Navigation
Support keyboard users:// ✅ Use Button component
<Button onPress={handleClick}>
Submit
</Button>
// ❌ Don't use div
<div onClick={handleClick}>Submit</div>
Screen Readers
Label everything:// ✅ Good labels
<TextField
label="Gift message"
value={message}
onChange={setMessage}
/>
<Icon
source="delivery"
accessibilityLabel="Delivery truck"
/>
Color Contrast
Ensure readability:
- Use Shopify UI components (compliant by default)
- Avoid custom colors
- Test with contrast checker
- Minimum 4.5:1 ratio
Focus States
Visible focus indicators:
- Shopify components have focus states
- Don’t remove with CSS
- Test with Tab key
- Ensure focus order logical
Security
Secure API calls:// ✅ Good: HTTPS, authentication
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ data }),
});
// ❌ Bad: HTTP, no auth
fetch('http://api.example.com/endpoint', {
method: 'POST',
body: data,
});
Never log or expose:// ❌ Bad: Logs sensitive data
console.log('Customer data:', customer);
console.log('Payment info:', paymentMethod);
// ✅ Good: Logs safely
console.log('Processing order:', { orderId: order.id });
console.log('Payment method type:', paymentMethod.type);
Deployment Best Practices
Test Locally First
npm run dev
# Test in browser
# Verify all features work
Review MCP Validation
Check validation output before deploying
- All APIs valid
- No deprecated components
- TypeScript compiles
- No console errors
Monitor Deployment
Watch logs during deployment:shopify app function logs --watch
Verify deployment succeeds Test in Dev Store
- Add items to cart
- Go through checkout
- Test all code paths
- Verify on mobile
Monitor Performance
- Check loading times
- Review error rates
- Monitor function execution time
- Track user feedback
Documentation
Write clear documentation:
# Delivery Estimate Extension
Shows estimated delivery dates in checkout.
## Features
- Calculates delivery based on shipping address
- Displays date range (e.g., "Nov 5-8")
- Handles international shipping
- Shows loading state while calculating
## Configuration
Configure in Shopify admin:
1. Settings → Checkout → Customize
2. Add "Delivery Estimate" block
3. Position after shipping address
4. Save
## Technical Details
- Target: `purchase.checkout.delivery-address.render-after`
- Uses: `useShippingAddress()` hook
- API: No external calls, pure calculation
- Performance: < 100ms load time
## Troubleshooting
**Not showing?**
- Verify added to checkout editor
- Check shipping address is entered
- Review console for errors
Next Steps
Debugging
Learn debugging techniques
Troubleshooting
Common issues and solutions
Extension Standards
Quality guidelines
Validation
MCP validation process