FileMaker Middleware: Architectural Patterns for Maintainable Solutions
You've built a FileMaker solution where users can submit expense reports. The "Submit Expense" button triggers a script that validates fields, checks approval limits, sends an email notification, updates the status, creates an audit log entry, and refreshes the layout. It works perfectly.
Six months later, you need to add a mobile interface. Then you want to allow submissions via the Data API. Suddenly you're copying that validation logic into three different scripts, trying to keep them synchronized. When the approval limit rules change, you update one script and forget the others. Users report inconsistencies. You're debugging the same logic in multiple places.
The problem isn't your FileMaker skills. It's architectural. When you tightly couple business logic to UI interactions, you create maintenance nightmares and make your solution rigid. The middleware pattern, borrowed from web development and adapted for FileMaker, solves this by introducing a layer between your interface and your business rules.
This article explores how to implement controller scripts in FileMaker that separate presentation from application logic, making your solutions more maintainable, testable, and adaptable to changing requirements.
What Is the Middleware Pattern?
In software architecture, middleware sits between the user interface and the core business logic, coordinating communication and managing complexity. It's the "controller" in Model-View-Controller (MVC) architecture.
For FileMaker developers, this means structuring your scripts into distinct layers:
Presentation Layer (View): Button actions, layout triggers, user interactions. These scripts are thin, handling only UI concerns like layout navigation, field highlighting, and displaying feedback messages.
Controller Layer (Middleware): Coordination scripts that orchestrate business operations. They call business logic scripts in the correct sequence, handle the overall workflow, and translate results back to the UI.
Business Logic Layer (Model): Core operational scripts that implement your application's rules. These scripts know nothing about layouts or UI. They receive parameters, perform operations, and return results.
This separation might seem like extra work initially, but it pays dividends as your FileMaker application architecture grows in complexity or needs to support multiple interfaces.
Why FileMaker Developers Need This Pattern
FileMaker's low-code nature encourages rapid development, which often means scripts that do everything in one place. This works for simple solutions but creates problems as complexity grows:
Code duplication: The same validation logic exists in multiple button scripts because there's no shared business logic layer to call.
Difficult testing: You can't test business rules without clicking through the entire UI workflow.
Interface lock-in: Changing from a card window to a popover requires rewriting scripts that mix navigation with business logic.
Mobile complexity: FileMaker Go requires different UI patterns, but your business rules should remain identical.
API integration challenges: When exposing functionality via the Data API or webhooks, you need the same business logic without the layout-dependent code.
The Three-Layer Architecture
Let's break down how to structure FileMaker scripts using the middleware pattern with a concrete example: processing an invoice payment.
Layer 1: Presentation Scripts (View)
These scripts respond to user actions and manage the UI. They're named to reflect their UI context:
- Button_ProcessPayment
- Trigger_OnRecordLoad_Invoice
- Navigation_GoToPaymentHistory
A presentation script for processing payment might look like:
Button_ProcessPayment
# Triggered by: Submit Payment button on Invoice layout
Set Error Capture [On]
Set Variable [$invoiceID; Value: Invoice::InvoiceID]
# Call controller script
Set Variable [$result; Value: Perform Script["Controller_ProcessInvoicePayment";
Parameter: JSONSetElement("{}";
["invoiceID"; $invoiceID; JSONNumber];
["amount"; Invoice::AmountDue; JSONNumber];
["paymentMethod"; Invoice::PaymentMethodSelected; JSONString]
)]]
# Handle result and update UI
If [JSONGetElement($result; "success")]
Show Custom Dialog ["Success"; "Payment processed successfully"]
Refresh Window
Close Card Window
Else
Show Custom Dialog ["Error"; JSONGetElement($result; "error")]
End If
Notice this script does three things only: gather UI data, call the controller, and display results. It knows nothing about payment processing rules.
Layer 2: Controller Scripts (Middleware)
Controller scripts orchestrate business operations without knowing about layouts or UI elements. They're named to reflect the operation:
- Controller_ProcessInvoicePayment
- Controller_CreateCustomerAccount
- Controller_GenerateMonthlyReport
A controller script coordinates multiple business logic scripts:
# Controller_ProcessInvoicePayment
# Purpose: Orchestrate invoice payment workflow
# Parameter: JSON with invoiceID, amount, paymentMethod
# Returns: JSON with success flag, data, and error message
Set Error Capture [On]
Set Variable [$param; Value: Get(ScriptParameter)]
# Extract parameters
Set Variable [$invoiceID; Value: JSONGetElement($param; "invoiceID")]
Set Variable [$amount; Value: JSONGetElement($param; "amount")]
Set Variable [$method; Value: JSONGetElement($param; "paymentMethod")]
# Validate inputs
Set Variable [$validation; Value: Perform Script["Business_ValidatePayment";
Parameter: $param]]
If [Not JSONGetElement($validation; "valid")]
Exit Script [Text Result: JSONSetElement("{}";
["success"; False; JSONBoolean];
["error"; JSONGetElement($validation; "error"); JSONString])]
End If
# Check business rules
Set Variable [$rulesCheck; Value: Perform Script["Business_CheckPaymentRules";
Parameter: $param]]
If [Not JSONGetElement($rulesCheck; "allowed")]
Exit Script [Text Result: JSONSetElement("{}";
["success"; False; JSONBoolean];
["error"; JSONGetElement($rulesCheck; "reason"); JSONString])]
End If
# Process payment
Set Variable [$paymentResult; Value: Perform Script["Business_RecordPayment";
Parameter: $param]]
If [Not JSONGetElement($paymentResult; "success")]
Exit Script [Text Result: $paymentResult]
End If
# Create audit trail
Perform Script ["Business_LogPayment"; Parameter: $paymentResult]
# Send notifications
Perform Script ["Business_SendPaymentConfirmation"; Parameter: $paymentResult]
# Return success with payment details
Exit Script [Text Result: JSONSetElement("{}";
["success"; True; JSONBoolean];
["paymentID"; JSONGetElement($paymentResult; "paymentID"); JSONNumber];
["timestamp"; Get(CurrentTimestamp); JSONString])]
The controller knows the sequence of operations but delegates actual work to business logic scripts. This keeps workflow coordination separate from implementation details.
Layer 3: Business Logic Scripts (Model)
Business logic scripts implement specific operations. They're pure logic with no UI dependencies:
- Business_ValidatePayment
- Business_CheckPaymentRules
- Business_RecordPayment
- Business_LogPayment
- Business_SendPaymentConfirmation
A business logic script focuses on one responsibility:
# Business_ValidatePayment
# Purpose: Validate payment data meets requirements
# Parameter: JSON with invoiceID, amount, paymentMethod
# Returns: JSON with valid flag and error message if invalid
Set Variable [$param; Value: Get(ScriptParameter)]
Set Variable [$invoiceID; Value: JSONGetElement($param; "invoiceID")]
Set Variable [$amount; Value: JSONGetElement($param; "amount")]
Set Variable [$method; Value: JSONGetElement($param; "paymentMethod")]
# Validation checks
If [IsEmpty($invoiceID)]
Exit Script [Text Result: JSONSetElement("{}";
["valid"; False; JSONBoolean];
["error"; "Invoice ID is required"; JSONString])]
End If
If [$amount <= 0]
Exit Script [Text Result: JSONSetElement("{}";
["valid"; False; JSONBoolean];
["error"; "Payment amount must be greater than zero"; JSONString])]
End If
If [IsEmpty($method) or not PatternCount("Credit Card¶ACH¶Check"; $method)]
Exit Script [Text Result: JSONSetElement("{}";
["valid"; False; JSONBoolean];
["error"; "Invalid payment method"; JSONString])]
End If
# All validations passed
Exit Script [Text Result: JSONSetElement("{}";
["valid"; True; JSONBoolean];
["error"; ""; JSONString])]
This script has zero UI code. It receives data, applies rules, returns results. You could call it from a button script, a server-side script, or the Data API with identical behavior.
Benefits of the Three-Layer Approach
Once you've structured your FileMaker script organization this way, several advantages emerge:
Reusability: The *Controller_ProcessInvoicePayment *script can be called from desktop layouts, mobile layouts, card windows, popovers, or even FileMaker Server scripts. The business logic doesn't change.
Testability: You can test *Business_ValidatePayment *by calling it with various parameters and checking the result JSON. No clicking required. Create a test script that calls each business logic script with edge cases and validates the responses.
Maintainability: When payment rules change, you update Business_CheckPaymentRules. Every interface that processes payments automatically uses the new rules because they all call the same controller and business logic.
Team collaboration: Different developers can work on UI improvements and business logic changes without conflicts. The controller layer provides a stable contract between the two.
Documentation through structure: The script organization itself documents the architecture. New developers can see the separation and understand where different types of code belong.
Implementing the Pattern in Existing Solutions
You don't need to refactor your entire FileMaker solution overnight. Introduce the middleware pattern incrementally:
Step 1: Identify a complex workflow that exists in multiple places or is difficult to maintain. Payment processing, order fulfillment, or approval workflows are good candidates.
Step 2: Extract business logic from the existing button script. Create new scripts for each distinct operation (validation, rule checking, data updates, notifications). These become your business logic layer.
Step 3: Create a controller script that calls the business logic scripts in sequence. Move all workflow coordination to this script.
Step 4: Simplify the button script to gather UI data, call the controller, and handle the response. This becomes your presentation layer.
Step 5: Test the refactored workflow thoroughly. The behavior should be identical, but the structure is now more maintainable.
Step 6: Reuse the controller when adding new interfaces. Need a mobile version? Create a new presentation script that calls the same controller. The business logic automatically works.
Naming Conventions for Clarity
Consistent naming makes the pattern self-documenting. Here's a naming strategy:
Presentation Scripts:
- Prefix: Button, Trigger*, Navigation_*
- Examples: Button_SubmitOrder, Trigger_OnRecordLoad, Navigation_GoToInvoices
Controller Scripts:
- Prefix: Controller_
- Examples: Controller_ProcessOrder, Controller_CreateCustomer, Controller_GenerateReport
Business Logic Scripts:
- Prefix: Business_
- Examples: Business_ValidateOrder, Business_CalculateDiscount, Business_SendNotification
This naming immediately signals which layer a script belongs to and what type of code you'll find inside. The FileMaker Script Workspace should organize these into folders matching the layers.
Parameter and Result Patterns
The middleware pattern depends on clear communication between layers. Standardize how scripts pass data:
Use JSON for complex parameters: When passing multiple values, use JSON objects. This self-documents the expected data and makes it easy to add parameters without breaking existing calls.
Return standardized result objects: Every controller and business logic script should return JSON with at minimum:
- success (boolean)
- data (object with operation-specific results)
- error (string message if success is false)
Example parameter JSON:
{
"customerID": 12345,
"orderItems": [
{"productID": 1, "quantity": 2},
{"productID": 5, "quantity": 1}
],
"shippingMethod": "Express"
}
Example result JSON:
{
"success": true,
"data": {
"orderID": 67890,
"totalAmount": 249.99,
"estimatedDelivery": "2025-01-20"
},
"error": ""
}
This consistency means any script calling a controller knows exactly what to expect and how to handle results.
The Data API Advantage
When you build with the middleware pattern, exposing functionality via FileMaker's Data API becomes straightforward. Your API endpoints can call the same controller scripts your UI uses.
A FileMaker Data API webhook that processes payments doesn't need different business logic. It constructs the parameter JSON from the API request, calls Controller_ProcessInvoicePayment, and returns the result JSON. The business rules are identical to the desktop interface because they're the same code.
This is impossible when business logic is embedded in button scripts with layout navigation commands and custom dialogs. The middleware pattern makes your solution API-ready by default.
Some Common Mistakes
Pitfall 1: Controllers that do too much Controllers should coordinate, not implement. If your controller has complex calculation logic or direct database manipulation, extract that into business logic scripts.
Pitfall 2: Business logic with UI dependencies If a business logic script uses Go to Layout, Show Custom Dialog, or references layout objects, it's not pure business logic. Remove UI dependencies or move that code to the presentation layer.
Pitfall 3: Skipping the controller layer Don't have presentation scripts call business logic scripts directly. The controller provides valuable coordination, error handling, and transaction management.
Pitfall 4: Inconsistent return formats Every controller and business logic script should return the same JSON structure. Inconsistency makes the pattern harder to use and undermines its benefits.
Pitfall 5: Over-engineering simple operations Not every button needs this full pattern. Simple navigation or single-field updates don't require three-layer architecture. Use the pattern where complexity justifies it.
Conclusion
The middleware pattern transforms FileMaker script architecture from tangled, monolithic scripts into organized, maintainable layers. By separating presentation, coordination, and business logic, you create solutions that adapt to new requirements without extensive rewrites.
The pattern requires discipline. It's tempting to add "just one quick layout navigation" to a business logic script or embed business rules directly in a button script. Resist that temptation. The pattern only works when you consistently maintain the separation.
Start small. Pick one complex workflow in your current solution and refactor it using this pattern. Feel the difference when you need to add a mobile interface or expose functionality via API. The investment in structure pays dividends immediately and continues paying as your solution evolves.
Your FileMaker solutions can follow the same architectural principles as professional web applications. The middleware pattern brings those principles to the FileMaker platform, elevating your development from ad-hoc scripting to intentional software architecture.
Your Next Steps
- Review a complex workflow in your current FileMaker project. Identify where business logic and UI code are mixed together. That's your refactoring opportunity.
- Create three script folders: Presentation, Controllers, and Business Logic. Start migrating scripts to the appropriate folders, even if you don't refactor them immediately. Organization is the first step.
- Pick one workflow and fully implement the three-layer pattern. Validate the inputs, apply business rules, coordinate operations, and handle results at the appropriate layers. Test it thoroughly.
- Then add a second interface to that workflow. Maybe you have a desktop button; add a mobile-optimized version that calls the same controller. Experience how much easier the second interface is when business logic is properly separated.
The middleware pattern isn't about writing more code. It's about writing better-organized code that adapts to change. Your future self will thank you for the clarity.
What workflow in your FileMaker solution would benefit most from this separation of concerns? Start there and build a more maintainable architecture one refactoring at a time.