migrationsoap-to-restapi-designtutorial

Migrate from SOAP to REST Without Rewriting the Upstream Service

SOAPless Team7 min read

Most teams do not start with permission to replace the upstream SOAP service. They start with a simpler need:

We need downstream teams to stop consuming raw SOAP.

That is why the practical migration path is usually not “rewrite everything.” It is “put a better boundary in front of the fixed service, then migrate consumers gradually.”

This guide is about that version of the problem.

Before You Start: Assess What You Have

Inventory Your SOAP Services

Before writing any code, document every SOAP service your system depends on:

  1. List all WSDL endpoints your applications consume
  2. Map operations to business functions — which SOAP calls support which features?
  3. Identify consumers — which applications, teams, or external partners call each service?
  4. Check operation frequency — high-traffic operations need the most careful migration
  5. Document authentication methods — WS-Security, Basic Auth, client certificates?
# Quick way to extract operations from a WSDL
curl -s "https://api.example.com/Service?wsdl" | \
  grep -oP 'operation name="\K[^"]+' | sort -u

Evaluate Whether You Should Migrate at All

Not every SOAP service needs to become a REST API. Ask these questions:

  • Is the SOAP service under your control? If it's a third-party service you consume, you can't change it — but you can put a translation layer in front of it.
  • Is the service stable? If it hasn't changed in years and works reliably, the cost of migration may not be justified.
  • Are there active consumers who expect SOAP? External partners using your SOAP API need advance notice and migration support.

Strategy 1: The Strangler Fig Pattern

The safest migration approach is the Strangler Fig pattern — you build new REST endpoints alongside the existing SOAP service and gradually redirect traffic until the SOAP service can be decommissioned.

Phase 1: Build a REST Facade

Create REST endpoints that internally call the existing SOAP service:

// New REST endpoint that wraps the existing SOAP call
app.get("/api/v1/users/:id", async (req, res) => {
  try {
    // Call the existing SOAP service internally
    const soapResponse = await soapClient.GetUserAsync({
      userId: parseInt(req.params.id),
    });

    // Transform to JSON response
    res.json({
      id: soapResponse.userId,
      name: soapResponse.userName,
      email: soapResponse.userEmail,
    });
  } catch (error) {
    // Translate SOAP faults to HTTP status codes
    if (error.message.includes("UserNotFound")) {
      return res.status(404).json({ error: "User not found" });
    }
    res.status(500).json({ error: "Internal server error" });
  }
});

This gives consumers a REST interface immediately without changing the backend.

Phase 2: Migrate Business Logic

Once the REST facade is stable, move the actual business logic from the SOAP service into the REST layer:

// Before: REST -> SOAP -> Database
// After: REST -> Database directly
app.get("/api/v1/users/:id", async (req, res) => {
  const user = await db.users.findById(parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: "User not found" });
  }
  res.json(user);
});

Phase 3: Redirect and Decommission

Once all consumers have switched to the REST endpoints and the REST layer no longer depends on the SOAP service:

  1. Monitor SOAP traffic logs for any remaining callers
  2. Set a decommission date and communicate it
  3. Return 410 Gone from the SOAP endpoint for a grace period
  4. Shut down the SOAP service

Strategy 2: The Proxy Approach

If you consume SOAP services you do not control (third-party APIs, partner services, government endpoints, or legacy systems owned by other teams), you cannot migrate the service itself. Instead, put a SOAP-to-REST proxy in front of it.

The proxy translates between REST/JSON on the consumer side and SOAP/XML on the service side:

Your App  →  JSON/REST  →  Proxy  →  SOAP/XML  →  Legacy Service
                                  ←  JSON  ←         ←  XML  ←

You can build this proxy yourself or use a purpose-built tool. SOAPless is designed for exactly this scenario: register the WSDL, get REST JSON endpoints, OpenAPI output, dashboard testing, and centralized SOAP auth handling without waiting for the upstream owner to redesign the contract.

Designing Your REST API

When creating REST endpoints to replace SOAP operations, follow these design principles:

Map Operations to Resources

SOAP is operation-centric (GetUser, CreateUser, DeleteUser). REST is resource-centric. Group related operations around resources:

SOAP OperationREST Endpoint
GetUserGET /api/users/{id}
CreateUserPOST /api/users
UpdateUserPUT /api/users/{id}
DeleteUserDELETE /api/users/{id}
SearchUsersGET /api/users?name=alice
GetUserOrdersGET /api/users/{id}/orders

Handle SOAP Faults as HTTP Status Codes

SOAP faults should map to appropriate HTTP status codes:

function soapFaultToHttpStatus(fault) {
  const mapping = {
    "soap:Client": 400,       // Bad request
    "soap:Server": 502,       // Bad gateway (upstream error)
    "AuthenticationFailed": 401,
    "AuthorizationFailed": 403,
    "ResourceNotFound": 404,
    "RateLimitExceeded": 429,
  };

  return mapping[fault.faultcode] || 500;
}

Version Your API from Day One

/api/v1/users/{id}

Versioning gives you room to evolve the REST API without breaking consumers. This is especially important during migration, when you may discover that the initial REST interface doesn't perfectly represent the underlying data model.

Testing Strategy

Run SOAP and REST in Parallel

During migration, run both interfaces simultaneously and compare responses:

async function compareResponses(userId) {
  const [soapResult, restResult] = await Promise.all([
    soapClient.GetUserAsync({ userId }),
    fetch(`/api/v1/users/${userId}`).then((r) => r.json()),
  ]);

  // Deep comparison
  assert.deepStrictEqual(
    normalizeUser(soapResult),
    normalizeUser(restResult),
    `Mismatch for user ${userId}`
  );
}

Contract Testing

Use tools like Pact or Dredd to verify that your REST API conforms to its OpenAPI spec. This catches regressions when you refactor the translation layer.

Load Testing

SOAP and REST have different performance characteristics. SOAP envelopes are larger (XML vs. JSON), but SOAP services sometimes batch operations differently. Load test your REST endpoints to ensure they handle the same traffic levels as the SOAP service.

Common Pitfalls

1. Losing SOAP Fault Detail

SOAP faults can carry structured detail elements with business-specific error information. Don't reduce everything to a generic 500 error:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Email address is invalid",
    "details": [
      { "field": "email", "reason": "Missing @ symbol" }
    ]
  }
}

2. Ignoring Stateful Operations

Some SOAP services maintain session state across calls (via SessionId headers or WS-ReliableMessaging). REST is stateless by design. You'll need to decide how to handle this — either maintaining state server-side in the REST layer or redesigning the flow.

3. Underestimating Type Differences

SOAP's xs:decimal has arbitrary precision. JSON's number is an IEEE 754 double. Financial calculations that depend on decimal precision can silently lose accuracy when converted to JSON numbers. Use string representations for precise decimal values:

{ "amount": "1234.56", "currency": "USD" }

4. Breaking External Consumers

If external partners consume your SOAP API, they need time and support to migrate. Provide:

  • A clear timeline (minimum 6 months notice)
  • Migration documentation
  • A sandbox environment with the new REST API
  • Direct engineering support during their transition

Timeline Expectations

A realistic timeline for a SOAP-to-REST migration:

PhaseDurationActivities
Assessment1-2 weeksInventory, dependency mapping
Design1-2 weeksREST API design, OpenAPI spec
Facade/Proxy2-4 weeksREST endpoints wrapping SOAP
Parallel testing2-4 weeksSide-by-side validation
Consumer migration4-12 weeksExternal partner transitions
Decommission2-4 weeksTraffic monitoring, shutdown

For a faster path, a SOAP-to-REST proxy like SOAPless can compress the Facade phase from weeks to hours. You still need the assessment, testing, and consumer migration phases, but the translation layer is handled for you.

Final Advice

Start with the lowest-risk, highest-value service first. Get the migration process right on a single service before scaling to the rest. Document everything — the next team to do this migration will thank you.