Laravel API Refactoring Without Breaking Changes

x32x01
  • 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 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
Each controller contained overlapping order creation logic. This made maintenance harder because a bug fix or business rule change might need to be applied in several places.

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 dedicated PlaceOrderAction.
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
To this:
  • Controller A → PlaceOrderAction
  • Controller B → PlaceOrderAction
  • Controller C → PlaceOrderAction
  • Other controllers → PlaceOrderAction
This creates a Single Source of Truth for the shared order creation process.

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.
However, centralizing the business logic does not automatically guarantee API compatibility. That requires a separate verification strategy.



🛡️ 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
        }
}
A refactoring change might accidentally rename 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.
Snapshots should represent intentional, supported behavior rather than blindly preserving every historical bug.
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.
The key principle is to keep internal implementation details separate from the public API contract.
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:
  1. Run unit tests for PlaceOrderAction.
  2. Run feature tests for each order creation endpoint.
  3. Compare the API responses against the contract snapshots.
  4. Investigate unexpected differences.
  5. Review and approve intentional contract changes.
  6. Deploy only after the required checks pass.
When a snapshot changes, developers should review the actual difference rather than automatically updating the stored snapshot. Otherwise, a breaking change could be accepted simply because the expected output was overwritten.
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 100% snapshot match was an important verification result, but it should not be interpreted as proof that every possible regression is impossible. Its value comes from combining contract snapshots with business logic tests, endpoint coverage, and appropriate deployment checks.



💡 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.
The goal is not to avoid every change. It is to make changes deliberately, with a clear understanding of which clients may be affected.



❓ 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.
 
Similar threads
x32x01
Replies
0
Views
7
x32x01
x32x01
x32x01
Replies
0
Views
88
x32x01
x32x01
x32x01
Replies
0
Views
74
x32x01
x32x01
x32x01
Replies
0
Views
151
x32x01
x32x01
x32x01
Replies
0
Views
145
x32x01
x32x01
Register & Login Faster
Forgot your password?
Forum Statistics
Threads
1,158
Messages
1,165
Members
16
Latest Member
b_a_s_m_a_l_a7
Back
Top