In the Shopify Architecture Guide, we cover the fundamentals. Today, a practical question that comes up in almost every Shopify project: Should I build this feature in the theme (Liquid) or in a custom app?
At smplx., we don't answer this question theoretically. We look at your use case, calculate costs, performance, and maintenance -- and then make an informed decision.
The Fundamental Differences
Theme Code (Liquid)
What is Liquid?
Liquid is Shopify's template language. Code in the theme runs in the storefront -- that is, in the customer's browser. This makes it fast for simple things, but also limited.
Architecture:
Customer Browser -> Liquid in Theme -> Rendered HTML
Pros:
- Simple deployment (Git push, Theme Editor)
- Immediately available after push
- Good performance for simple things (no API call needed)
- Close to catalog data (Products, Collections, etc.)
- No additional infrastructure
Cons:
- No server-side logic (security risk with sensitive data)
- Limits with complex business logic
- Limited debugging tools
- No access to Admin API
- Hard to test automatically
Custom App
What is a Custom App?
A custom app is code that runs on Shopify's servers (backend) or on your own servers. It has access to the Admin API and can work directly with shop data.
Architecture:
Customer Browser -> Shopify Admin / Your App -> Liquid in Theme (optional) -> Rendered HTML
Pros:
- Access to Admin API (write operations!)
- Server-side logic = secure processing
- Webhooks for event-driven architecture
- Scalable for complex scenarios
- Better debugging tools
- Automated testing possible
Cons:
- More complex deployment process
- API rate limits to consider
- More infrastructure (Server / Shopify Platform)
- Higher latency for real-time data
- Costs for hosting/infrastructure
Real-World Decisions: Concrete Examples
Example 1: Dynamic Discount Logic
Scenario: You want to automatically give customers discounts based on purchase history.
Option 1: In the Theme (Liquid)
{% if customer %}
{% if customer.orders_count > 10 %}
<p class="discount-banner">
VIP Customer! +10% on everything!
</p>
{% endif %}
{% endif %}
Problem:
- Only UI display, doesn't apply to the cart
- No access to order data in real-time
- No automation possible
Option 2: Custom App with Admin API
// On Product-Add-To-Cart (via Webhook)
const app = express();
app.post('/webhooks/orders/create', async (req, res) => {
const order = req.body.order;
const customer = order.customer;
if (customer.orders_count > 10) {
// Admin API: Automatically create discount code
await shopify.post('/admin/api/2024-01/discount_codes.json', {
discount_code: {
price_rule_id: VETERAN_RULE_ID,
code: `VIP_${customer.id}`,
}
});
}
res.status(200).send({ success: true });
});
Decision: Custom App (Webhooks + Admin API) Reason: Automatic discount application requires server-side logic
Example 2: Custom Checkout Extension
Scenario: You want a custom field in the checkout (e.g., "preferred delivery date").
Option 1: Theme Liquid + JavaScript
<input type="date" name="attributes[preferred_delivery_date]">
<script>
// Validation in the browser
document.querySelector('form').addEventListener('submit', (e) => {
const date = document.querySelector('input[name*="delivery_date"]').value;
if (new Date(date) < new Date()) {
e.preventDefault();
alert('Please choose a future date');
}
});
</script>
Problem:
- Checkout is too sensitive for browser-only validation
- No connection to fulfillment logic
- No notification to the warehouse
Option 2: Checkout Extension (App Block)
// Extension on Checkout UI (modern Shopify API)
import { useExtensionInput } from '@shopify/checkout-ui-extensions-react';
import { Box, Text, Select } from '@shopify/checkout-ui-extensions-react';
export default function DeliveryDateExtension() {
const { extensionPoint } = useExtensionInput();
return (
<Box border="base" padding="base">
<Text>Preferred Delivery Date:</Text>
<Select
options={generateFutureDate()}
onChange={(value) => applyAttributes({ delivery_date: value })}
/>
</Box>
);
}
Decision: Checkout Extension (modern app architecture) Reason: Checkout is Shopify core -- extensions are the right place
Example 3: Bekateq Case Study
Bekateq originally had a store with 5+ custom apps for various functions. The complexity was no longer manageable.
Problem:
- Each app needed monitoring
- Different deployments
- Rate limiting was critical
- Maintenance costs were exploding
Solution: Bekateq migrated to a single custom app with 4,400+ lines and zero dependencies (except the Shopify SDK).
Key Learning: Not every small function needs a separate app. Fewer, well-thought-out apps > many small apps.
Decision Matrix: When Do I Use What?
| Feature | Theme (Liquid) | Custom App | Checkout Extension |
|---|---|---|---|
| Product Page UI | Yes | No | No |
| Collection Filters | Yes | Optional | No |
| Cart Logic | View Only | Yes | Yes |
| Checkout Field | Unsafe | Possible | Yes |
| Discount Automation | No | Yes | No |
| Webhook Processing | No | Yes | No |
| Admin Panel | No | Yes | No |
| Real-time Sync with External System | No | Yes | No |
Performance Comparison
Scenario: Product is Added to Collection
Theme-Only (Liquid Loop):
Browser Request -> Server renders -> HTML with Loop
Rendering Time: ~200ms (depends on collection size)
Custom App (GraphQL Admin API):
Frontend Request -> Admin API Call -> Backend Logic -> Webhook -> Theme Update
Latency: ~500-1000ms (API call + webhook processing)
Best Practice:
- Time-critical (< 500ms): Theme
- Not time-critical (automation, background jobs): Custom App
Shopify App Bridge: The Bridge Between Both Worlds
App Bridge allows you to write JavaScript in the theme that communicates with a custom app without reloading the page.
Example: Theme Talks to App
// In the Theme Liquid JavaScript
const client = ShopifyApp.apiClient;
document.querySelector('.add-to-cart').addEventListener('click', async () => {
// Calls Custom App backend
const response = await fetch('/api/custom-app/add-to-cart-hook', {
method: 'POST',
body: JSON.stringify({ productId: '123', quantity: 1 })
});
const result = await response.json();
if (result.allowed) {
// Continue with standard checkout
} else {
alert(result.reason);
}
});
Advantage: Theme UI with app logic. Best of both worlds combined.
Admin API: When Does Your App Need Write Access?
The Admin API has two scopes:
- read_products: Read data
- write_products: Write data
Write access is expensive -- costs more API calls and requires more testing.
Do you need write access for:
- No: Displaying data (read-only access)
- No: Displaying a filtered list (read-only access)
- Yes: Creating/editing products (write access!)
- Yes: Inventory sync (write access!)
- Yes: Automatic discount creation (write access!)
Mistakes We See Often
Mistake 1: "I'll Build Everything into One Massive Custom App"
Problem: 5,000+ lines, hard to test, deployment anxiety.
Solution: Split by responsibility. Bekateq's 4,400 lines were highly focused.
Mistake 2: "I'll Use Liquid for Secure Operations"
Problem: Customer data, discounts, pricing -- all visible in the source code.
Solution: Everything that's sensitive belongs in a custom app.
Mistake 3: "Rate Limits? We'll Deal with That Later"
Problem: App runs slowly, gets 429 errors, customers see errors.
Solution: From day 1: Implement rate limit handling.
Our Architecture Principles at smplx.
1. Liquid for Presentation Theme code is only for UI. No business-critical logic.
2. Admin API Only When Necessary Read access where possible, write access only for real admin operations.
3. Webhooks for Automation Don't poll the API every 10 seconds -- webhooks trigger the logic.
4. Testing Culture Custom apps must be automatically testable.
5. Minimal Apps One responsibility per app. One big clear purpose.
The Decision Checklist
Ask yourself:
- Is this data-sensitive? -> Custom App
- Do I need Admin API access? -> Custom App
- Does this run automatically (webhook)? -> Custom App
- Must this be super fast (< 200ms)? -> Theme
- Is this only UI display? -> Theme
- Must it be testable? -> Custom App
- Does it need infrastructure monitoring? -> Custom App
If more than 2 points indicate Custom App -> Custom App.
Costs: Theme vs. Custom App
Theme Development:
- 10k-50k EUR one-time costs
- 100-500 EUR/month maintenance
- Low scaling costs
Custom App:
- 30k-150k EUR one-time development
- 500-2,000 EUR/month infrastructure + monitoring
- Higher scaling costs (per request)
However: A custom app with a clear purpose is cheaper than 3 theme hacks that you have to fix every month.
Long-term Perspective
After 2-3 years, it looks like this:
Theme-Only Approach:
Year 1: Cost: $30k (Dev) + $1.2k (Hosting)
Year 2: Cost: $10k (Bug-Fixes) + $1.2k (Hosting)
Year 3: Cost: $20k (Big Feature) + $1.2k (Hosting)
Total: $64.6k
Problem: Code became too complex, can't change anymore
App-Based Approach (Bekateq-Style):
Year 1: Cost: $80k (Dev) + $6k (Hosting)
Year 2: Cost: $5k (Bug-Fixes) + $6k (Hosting)
Year 3: Cost: $10k (New Feature) + $6k (Hosting)
Total: $113k
Advantage: Code is maintainable, new features take 5 days instead of 15
After year 3, you pay less for new features because development is 3x faster.
About the Author
Claudio Gerlich is Technical Architect at smplx. and has been specialized in Shopify technology since 2020. With smplx., we've architecturally advised over 50+ stores -- from simple theme customizations to complex multi-app systems.
Bekateq is a great example: We helped them restructure their app architecture from chaotic to elegant.
smplx. is a Shopify Technical Partner (since 2020) based in Munsterland, NRW.