- by x32x01 ||
Refactoring six Laravel controllers sounds straightforward until you realize that existing mobile apps depend on the exact API responses those controllers return. A small change to a response structure can break production clients, even when the underlying business logic works perfectly.
In an e-commerce system, the challenge was to eliminate duplicated order creation logic across six different payment flows without introducing breaking changes for existing mobile apps.
The solution combined a shared
More importantly, the controllers could return slightly different API responses. Over time, these differences became part of the behavior that existing mobile apps expected.
For example, a mobile client might rely on a particular response field, its data type, or the structure of a nested object. Removing a field or changing its type during refactoring could cause unexpected errors in production.
The goal was clear: eliminate duplicated business logic while preserving the existing API contract.
Instead of maintaining the same business rules across six controllers, each relevant controller could delegate the operation to this shared action.
Conceptually, the flow changes from this:
The main benefits are:
In practice, mobile applications may depend on response details that were never formally documented.
To protect those expectations, the team created 23 contract snapshots representing existing API responses.
A snapshot captures the expected output for a specific test scenario. After refactoring, the application generates a new response and compares it with the stored snapshot.
If an unexpected difference appears, the test can flag it before deployment.
For example, consider an existing API response:
A refactoring change might accidentally rename
Even if the order is created successfully, an existing mobile app could fail if it expects the original structure.
Contract snapshots help detect these unintended changes before they reach production.
A snapshot test can also pass when both the stored snapshot and the implementation share the same incorrect behavior. For this reason, snapshots should complement assertions about business rules and expected outcomes.
A compatibility layer separates the new internal implementation from the response format existing clients expect.
For example, the application can use a shared action to create an order, then transform its result into the established response structure before returning it to the client.
This approach makes it possible to improve the internal architecture without forcing users to update their mobile apps immediately.
A compatibility layer can help preserve:
However, compatibility code should not become another source of duplication. Keep response transformations centralized where practical, document legacy behavior, and remove obsolete compatibility rules only when it is safe to do so.
The 23 snapshots were used to compare the expected API responses with the responses produced by the refactored implementation.
Integrating these tests into the CI/CD pipeline helps catch unintended changes before the code reaches production.
A practical workflow looks like this:
For APIs used by mobile clients, it is also worth testing older client assumptions explicitly and monitoring production errors after deployment.
If a production mobile app depends on a particular response structure, that structure is part of the practical contract between the client and the server, even if it was never formally documented.
Successful refactoring means improving the internal implementation while preserving the behavior that other systems rely on.
For an API used by existing mobile apps, a safer strategy is to:
Do you rely on Laravel feature tests, JSON snapshots, OpenAPI contract testing, consumer-driven contract tests, or a combination of these approaches?
Share your experience and testing strategy in the comments.
In an e-commerce system, the challenge was to eliminate duplicated order creation logic across six different payment flows without introducing breaking changes for existing mobile apps.
The solution combined a shared
PlaceOrderAction, 23 contract snapshots, and a compatibility layer to preserve existing API behavior.🛒 The Problem: Duplicated Order Creation Logic
The application had six different paths for creating orders, each handling a particular payment method or checkout flow, including:- 💵 Cash on Delivery (COD)
- 👛 Digital wallet payments
- 💳 Online payment gateways
- 🔄 Other checkout and payment flows
More importantly, the controllers could return slightly different API responses. Over time, these differences became part of the behavior that existing mobile apps expected.
For example, a mobile client might rely on a particular response field, its data type, or the structure of a nested object. Removing a field or changing its type during refactoring could cause unexpected errors in production.
The goal was clear: eliminate duplicated business logic while preserving the existing API contract.
🏗️ Step 1: Centralize Order Creation with PlaceOrderAction
The first step was to move the shared order creation logic into a dedicatedPlaceOrderAction.Instead of maintaining the same business rules across six controllers, each relevant controller could delegate the operation to this shared action.
Conceptually, the flow changes from this:
- Controller A → Order creation logic
- Controller B → Order creation logic
- Controller C → Order creation logic
- Other controllers → Duplicated logic
- Controller A →
PlaceOrderAction - Controller B →
PlaceOrderAction - Controller C →
PlaceOrderAction - Other controllers →
PlaceOrderAction
The main benefits are:
- ✅ Business rules are maintained in one place.
- ✅ Bug fixes and improvements are easier to apply consistently.
- ✅ Controllers become smaller and easier to understand.
- ✅ Automated tests can focus on the shared action and the behavior of individual API endpoints.
🛡️ Step 2: Protect the API Contract with 23 Contract Snapshots
An API contract describes how a client communicates with an API, including request formats, response structures, field types, status codes, and relevant error behavior.In practice, mobile applications may depend on response details that were never formally documented.
To protect those expectations, the team created 23 contract snapshots representing existing API responses.
A snapshot captures the expected output for a specific test scenario. After refactoring, the application generates a new response and compares it with the stored snapshot.
If an unexpected difference appears, the test can flag it before deployment.
For example, consider an existing API response:
JSON:
{
"success": true,
"order": {
"id": 125,
"status": "pending",
"total": 499.99
}
} id to order_id, remove success, or return total as a string instead of a number.Even if the order is created successfully, an existing mobile app could fail if it expects the original structure.
Contract snapshots help detect these unintended changes before they reach production.
What Should API Snapshots Cover?
Useful snapshot scenarios include:- Successful order creation for each relevant payment flow.
- Missing or optional response fields.
- JSON field types and nested object structures.
- HTTP status codes and error response formats.
- Payment-specific response differences.
- Relevant edge cases, such as validation failures.
A snapshot test can also pass when both the stored snapshot and the implementation share the same incorrect behavior. For this reason, snapshots should complement assertions about business rules and expected outcomes.
🔄 Step 3: Add a Compatibility Layer for Existing Mobile Apps
Even when the shared business logic is correct, the new implementation might produce a response that differs from the legacy format.A compatibility layer separates the new internal implementation from the response format existing clients expect.
For example, the application can use a shared action to create an order, then transform its result into the established response structure before returning it to the client.
This approach makes it possible to improve the internal architecture without forcing users to update their mobile apps immediately.
A compatibility layer can help preserve:
- Existing JSON field names.
- Expected field types and nesting.
- Legacy status values and response conventions.
- Payment-specific response formats.
- Established error responses where backward compatibility is required.
However, compatibility code should not become another source of duplication. Keep response transformations centralized where practical, document legacy behavior, and remove obsolete compatibility rules only when it is safe to do so.
🚀 Step 4: Run Contract Tests in CI/CD Before Deployment
Automated tests are most useful when they run consistently as part of the development workflow.The 23 snapshots were used to compare the expected API responses with the responses produced by the refactored implementation.
Integrating these tests into the CI/CD pipeline helps catch unintended changes before the code reaches production.
A practical workflow looks like this:
- Run unit tests for
PlaceOrderAction. - Run feature tests for each order creation endpoint.
- Compare the API responses against the contract snapshots.
- Investigate unexpected differences.
- Review and approve intentional contract changes.
- Deploy only after the required checks pass.
For APIs used by mobile clients, it is also worth testing older client assumptions explicitly and monitoring production errors after deployment.
📊 The Results
The refactoring achieved four important outcomes:- ✅ Order creation logic was centralized in one
PlaceOrderAction. - ✅ All 23 contract snapshots matched the expected responses, achieving a 100% snapshot pass rate.
- ✅ Existing mobile apps retained compatibility with the API.
- ✅ CI/CD checks could detect unintended response changes before they reached production.
💡 The Biggest Lesson: An API Contract Is More Than Documentation
Postman collections and Swagger or OpenAPI specifications are useful for documenting APIs, but documentation alone cannot prevent regressions.If a production mobile app depends on a particular response structure, that structure is part of the practical contract between the client and the server, even if it was never formally documented.
Successful refactoring means improving the internal implementation while preserving the behavior that other systems rely on.
For an API used by existing mobile apps, a safer strategy is to:
- Centralize duplicated business logic.
- Capture current behavior with representative contract tests.
- Preserve legacy response formats through a compatibility layer when necessary.
- Run regression tests automatically in CI/CD.
- Treat intentional API changes as versioned or explicitly reviewed changes.
❓ Frequently Asked Questions
----------------------What is API contract testing?
API contract testing verifies that an API follows the expected request and response rules, including field names, data types, status codes, and response structure. It helps detect breaking changes before clients encounter them.Are snapshot tests enough to prevent API regressions?
No. Snapshots are useful for detecting unexpected output changes, but they should be combined with assertions, business logic tests, endpoint tests, and validation of important edge cases.How can Laravel developers refactor controllers safely?
Start by identifying duplicated business logic, move shared operations into an appropriate action or service, and preserve the existing response behavior. Add regression tests before changing the implementation, then run them throughout the refactoring process.How do you avoid breaking older mobile app versions?
Maintain backward-compatible response formats, use a compatibility layer when needed, and introduce breaking changes through a deliberate migration or API versioning strategy rather than changing existing responses unexpectedly.💬 A Question for Laravel Developers
How do you prevent API response regressions when refactoring a large Laravel application that is already running in production?Do you rely on Laravel feature tests, JSON snapshots, OpenAPI contract testing, consumer-driven contract tests, or a combination of these approaches?
Share your experience and testing strategy in the comments.