---
title: "Migrating from Apigee to Zuplo: Policy Mapping, Architecture Translation, and Step-by-Step Guide"
description: "A complete guide to migrating from Apigee Edge to Zuplo — covering policy mapping, architecture differences, developer portal migration, and zero-downtime cutover strategies."
canonicalUrl: "https://zuplo.com/learning-center/migrating-from-apigee-to-zuplo"
pageType: "learning-center"
authors: "nate"
tags: "API Gateway, API Best Practices"
image: "https://zuplo.com/og?text=Migrating%20from%20Apigee%20to%20Zuplo%3A%20Policy%20Mapping%20and%20Step-by-Step%20Guide"
---
Apigee Edge is winding down. The final Private Cloud (OPDK) version, 4.53.01,
reaches end of life on February 26, 2027. The Classic UI shut down on November
2, 2025. Public Cloud customers are getting increasingly urgent migration
notices. If you're running APIs on Apigee Edge, the question isn't _whether_ to
migrate — it's _where_.

Google's recommended path is Apigee X, but that migration is notoriously
complex. Apigee X is not an in-place upgrade — it's a different platform with
different infrastructure, APIs, and deployment models. SADA, a Google Cloud
partner, describes the Edge-to-X migration as "a daunting task that requires
lots of careful planning, scripting, testing, and coordination."

This guide walks you through migrating from Apigee to [Zuplo](https://zuplo.com)
— mapping every policy, translating architecture concepts, and cutting over with
zero downtime. For the business case behind this move, see our companion post on
[Apigee Edge reaching end of life](/blog/apigee-edge-end-of-life-migrate-to-zuplo).

## What This Guide Covers

1. [Architecture Comparison](#architecture-comparison)
2. [Policy-to-Policy Mapping](#policy-to-policy-mapping)
3. [Complete Policy Reference](#complete-policy-reference)
4. [Developer Portal Migration](#developer-portal-migration)
5. [API Product and App Migration](#api-product-and-app-migration)
6. [Step-by-Step Migration Process](#step-by-step-migration-process)
7. [Handling Custom Apigee Policies](#handling-custom-apigee-policies)
8. [Analytics and Monitoring Migration](#analytics-and-monitoring-migration)
9. [Common Migration Pitfalls](#common-migration-pitfalls)
10. [Post-Migration Optimization](#post-migration-optimization)

---

## Architecture Comparison

Before diving into policy mapping, it helps to understand how Apigee and Zuplo
differ architecturally. The two platforms share the same goal — managing API
traffic — but take fundamentally different approaches.

### Apigee's Architecture

Apigee uses a **proxy + target model**. You define API proxies that intercept
client requests, apply policies (written in XML), and forward traffic to target
endpoints. Configuration is split across proxy bundles, shared flows, target
servers, environment configurations, and key-value maps. Deployment is
region-bound — tied to Google Cloud Platform in the case of Apigee X, or to your
own infrastructure for Private Cloud.

Custom logic lives in **JavaScript callouts** (running on a proprietary
runtime), **Java callouts** (compiled JAR files deployed with your proxy
bundle), or **Python scripts** (limited support). Apigee's Trireme-based Node.js
runtime
[already reached end of life](https://docs.apigee.com/release/notes/trireme-nodejs-eol),
further limiting options.

### Zuplo's Architecture

Zuplo is an **edge-native, programmable API gateway**. Your API configuration is
a standard OpenAPI document (`routes.oas.json`) extended with Zuplo-specific
annotations. Policies are defined declaratively in `policies.json` and executed
in a pipeline — inbound policies run before the request reaches your backend,
outbound policies run before the response reaches the client.

Custom logic is written in **TypeScript** and runs on a Web Standards-based
runtime (standard `Request`, `Response`, and `fetch` APIs). Everything lives in
Git — routes, policies, custom code, environment variables, and developer portal
configuration. Push to `main` and your gateway deploys globally across
[300+ edge locations](https://zuplo.com/docs/articles/hosting-options) in under
20 seconds.

### Key Architectural Differences

- **Configuration format**: Apigee uses XML policy bundles. Zuplo uses JSON
  (OpenAPI + policies) and TypeScript.
- **Deployment model**: Apigee is region-bound (GCP for Apigee X). Zuplo deploys
  to 300+ global edge locations on any cloud.
- **Custom logic runtime**: Apigee uses proprietary JavaScript/Java callouts.
  Zuplo uses standard TypeScript with Web APIs.
- **Configuration management**: Apigee requires custom CI/CD tooling. Zuplo is
  [GitOps-native](https://zuplo.com/docs/articles/source-control) — push to Git
  and it deploys.
- **Developer portal**: Apigee uses a separate Drupal-based portal. Zuplo
  [auto-generates a portal](https://zuplo.com/docs/dev-portal/introduction) from
  your OpenAPI spec.

## Policy-to-Policy Mapping

This is the core of any Apigee migration. Every Apigee policy has a Zuplo
equivalent — either a built-in policy, a configuration option, or a short
TypeScript module.

### Traffic Management Policies

**SpikeArrest → Rate Limiting**

Apigee's SpikeArrest policy smooths traffic spikes by limiting the peak request
rate. In Zuplo, the
[rate-limit-inbound](https://zuplo.com/docs/policies/rate-limit-inbound) policy
provides equivalent functionality with more flexibility — you can limit by IP,
authenticated user, API key, or a custom function.

```json
{
  "name": "rate-limit",
  "policyType": "rate-limit-inbound",
  "handler": {
    "export": "RateLimitInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "rateLimitBy": "ip",
      "requestsAllowed": 100,
      "timeWindowMinutes": 1
    }
  }
}
```

**Quota → Rate Limiting (per-user)**

Apigee's Quota policy enforces long-term usage limits per developer or app. In
Zuplo, you use the same `rate-limit-inbound` policy with `rateLimitBy` set to
`"user"` or `"function"` for per-consumer tier limits.

### Authentication Policies

**VerifyAPIKey → API Key Authentication**

Apigee's VerifyAPIKey policy validates API keys sent as query parameters or
headers. Zuplo's
[api-key-inbound](https://zuplo.com/docs/policies/api-key-inbound) policy does
the same, backed by a globally distributed API key service.

```json
{
  "name": "api-key-auth",
  "policyType": "api-key-inbound",
  "handler": {
    "export": "ApiKeyInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {}
  }
}
```

**OAuthV2 (VerifyAccessToken) → JWT Authentication**

Apigee's OAuthV2 policy with the `VerifyAccessToken` operation validates OAuth
2.0 bearer tokens. In Zuplo, the
[open-id-jwt-auth-inbound](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound)
policy validates JWTs from any OpenID-compliant provider. Zuplo also offers
provider-specific policies for
[Auth0](https://zuplo.com/docs/policies/auth0-jwt-auth-inbound),
[Okta](https://zuplo.com/docs/policies/okta-jwt-auth-inbound),
[Clerk](https://zuplo.com/docs/policies/clerk-jwt-auth-inbound),
[Supabase](https://zuplo.com/docs/policies/supabase-jwt-auth-inbound),
[AWS Cognito](https://zuplo.com/docs/policies/cognito-jwt-auth-inbound), and
[Firebase](https://zuplo.com/docs/policies/firebase-jwt-inbound).

**BasicAuthentication → Basic Auth**

Apigee's BasicAuthentication policy decodes Base64-encoded credentials. Zuplo's
`basic-auth-inbound` policy provides the same functionality.

### Message Transformation Policies

**AssignMessage → Header / Body / Query Param Policies**

Apigee's AssignMessage is a multipurpose policy for setting headers, query
parameters, form parameters, and the request body. In Zuplo, these capabilities
are split into focused policies:

- Set headers →
  [set-headers-inbound](https://zuplo.com/docs/policies/set-headers-inbound)
- Remove headers →
  [remove-headers-inbound](https://zuplo.com/docs/policies/remove-headers-inbound)
- Set query parameters →
  [set-query-params-inbound](https://zuplo.com/docs/policies/set-query-params-inbound)
- Set request body →
  [set-body-inbound](https://zuplo.com/docs/policies/set-body-inbound)

**XMLToJSON / JSONToXML → XML to JSON Outbound**

Apigee's format conversion policies have a direct equivalent in Zuplo's
[xml-to-json-outbound](https://zuplo.com/docs/policies/xml-to-json-outbound)
policy, which is especially useful for modernizing legacy SOAP backends.

**ExtractVariables → Custom Code**

Apigee's ExtractVariables policy parses variables from request/response payloads
using JSON paths, XML paths, or regex. In Zuplo, you handle this with a
[custom-code-inbound](https://zuplo.com/docs/policies/custom-code-inbound)
policy — a TypeScript function that extracts values and stores them on
`context.custom` for downstream policies to use.

### Mediation Policies

**RaiseFault → Custom Code (return Response)**

Apigee's RaiseFault policy generates custom error responses. In Zuplo, any
inbound policy can short-circuit the pipeline by returning a `Response` object:

```typescript
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  if (!request.headers.get("x-required-header")) {
    return new Response(JSON.stringify({ error: "Missing required header" }), {
      status: 400,
      headers: { "content-type": "application/json" },
    });
  }
  return request;
}
```

**ServiceCallout → fetch() in Custom Code**

Apigee's ServiceCallout policy makes HTTP requests to external services during
the proxy flow. In Zuplo, you use the standard `fetch()` API inside a custom
code policy:

```typescript
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  const authResponse = await fetch(
    "https://auth-service.example.com/validate",
    {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ token: request.headers.get("authorization") }),
    },
  );

  if (!authResponse.ok) {
    return new Response("Unauthorized", { status: 401 });
  }

  const authData = await authResponse.json();
  context.custom.userId = authData.userId;

  return request;
}
```

### Routing and Target Policies

**RouteRules / TargetEndpoint → URL Forward or URL Rewrite Handler**

Apigee's proxy-to-target routing is handled by Zuplo's
[URL Forward Handler](https://zuplo.com/docs/handlers/url-forward) (appends the
incoming path to a base URL) or
[URL Rewrite Handler](https://zuplo.com/docs/handlers/url-rewrite) (rewrites
URLs using template interpolation).

### Security Policies

**AccessControl → ACL Policy**

Apigee's AccessControl policy restricts access by IP address. Zuplo's
`acl-policy-inbound` policy provides similar IP-based access control.

**MessageValidation → Request Validation**

Apigee's message validation policies (SOAPMessageValidation,
JSONThreatProtection, XMLThreatProtection) map to Zuplo's
[request-validation-inbound](https://zuplo.com/docs/policies/request-validation-inbound)
policy, which validates requests against your OpenAPI schema definitions —
including body, query parameters, path parameters, and headers.

## Complete Policy Reference

Here is a quick-reference mapping of the most common Apigee policies to their
Zuplo equivalents:

- **SpikeArrest** → `rate-limit-inbound`
- **Quota** → `rate-limit-inbound` (with `rateLimitBy: "user"` or `"function"`)
- **VerifyAPIKey** → `api-key-inbound`
- **OAuthV2 (VerifyAccessToken)** → `open-id-jwt-auth-inbound` (or
  provider-specific JWT policies)
- **BasicAuthentication** → `basic-auth-inbound`
- **AssignMessage (headers)** → `set-headers-inbound` / `remove-headers-inbound`
- **AssignMessage (query params)** → `set-query-params-inbound`
- **AssignMessage (body)** → `set-body-inbound`
- **XMLToJSON** → `xml-to-json-outbound`
- **ExtractVariables** → `custom-code-inbound` (TypeScript)
- **JavaScript / JavaCallout** → `custom-code-inbound` / `custom-code-outbound`
  (TypeScript)
- **ServiceCallout** → `custom-code-inbound` with `fetch()`
- **RaiseFault** → Return a `Response` from any custom policy
- **AccessControl** → `acl-policy-inbound`
- **JSONThreatProtection** → `request-validation-inbound`
- **ResponseCache** → Handled at the edge automatically, or via custom code
- **KeyValueMapOperations** → Environment variables + `context.custom`
- **FlowCallout (Shared Flows)** → Composite policies (`composite-inbound` /
  `composite-outbound`)

## Developer Portal Migration

Apigee's developer portal is typically Drupal-based — requiring separate
hosting, CMS management, theme customization, and manual content updates. Many
teams have invested significant effort in customizing their Drupal portal with
custom themes, documentation pages, and interactive features.

Zuplo takes a fundamentally different approach. Your
[developer portal](https://zuplo.com/docs/dev-portal/introduction) is
auto-generated directly from your OpenAPI spec. Every route, parameter, and
schema you define in `routes.oas.json` automatically appears in your portal
with:

- **Interactive API documentation** — auto-generated from your OpenAPI spec,
  always in sync with your gateway configuration
- **API Playground** — developers can test live endpoints directly from the
  portal
- **Self-serve API key management** — developers sign up, create keys, and
  manage their own access without admin intervention
- **Custom pages** — add rich documentation with MDX (Markdown + JSX components)
- **Custom themes** — full visual customization with Tailwind CSS and shadcn
  themes
- **Custom domains** — host your portal on `docs.yourcompany.com`

### Migration Steps for Your Developer Portal

1. **Export your OpenAPI specs from Apigee** — use the Apigee management API or
   download from the Apigee UI
2. **Import into Zuplo** — your specs become both your gateway configuration and
   your portal documentation
3. **Add custom documentation** — migrate any guides, tutorials, or reference
   pages from Drupal to MDX custom pages in Zuplo
4. **Configure authentication** — set up developer sign-in using Auth0, Clerk,
   Supabase, or any OAuth provider
5. **Update DNS** — point your developer portal domain to Zuplo

The biggest win here is eliminating the Drupal dependency entirely. No more CMS
upgrades, no more theme compatibility issues, no more manually syncing API
documentation with your actual API behavior.

## API Product and App Migration

Apigee uses a hierarchy of **API Products**, **Developer Apps**, and **API
Keys** to control access. Here's how these concepts translate to Zuplo:

### Apigee API Products → Zuplo Routes + Policies

In Apigee, an API Product bundles specific API proxy paths with quota limits and
OAuth scopes. In Zuplo, you achieve this by:

- Defining routes in your OpenAPI spec (`routes.oas.json`)
- Attaching policies (rate limiting, authentication) per route
- Using consumer metadata to control per-consumer access levels

### Apigee Developer Apps → Zuplo Consumers

In Apigee, a Developer App represents an application registered by a developer,
with one or more API keys. In Zuplo,
[consumers](https://zuplo.com/docs/articles/api-key-management) serve the same
purpose:

- Each **consumer** represents a person, organization, or service
- Consumers can have **one or more API keys**
- Consumers carry **metadata** (plan tier, organization name, custom attributes)
  accessible at runtime via `request.user.data`
- Consumers can be **tagged** for grouping and management

### Migrating API Keys

If you need to migrate existing API keys from Apigee (to avoid breaking existing
integrations during the transition), you can use Zuplo's
[Developer API](https://zuplo.com/docs/articles/api-key-management) to
programmatically create consumers and import keys. A typical migration script
would:

1. Export developer apps and keys from Apigee's management API
2. Create corresponding consumers in Zuplo with matching metadata
3. Import the API keys so existing client integrations continue working
4. Run both gateways in parallel during the transition period

## Step-by-Step Migration Process

### Phase 1: Pre-Migration Audit

Before touching any configuration, inventory everything your Apigee setup does.

**Routes and proxies:**

- [ ] List every API proxy and its base path
- [ ] Document all route rules and target endpoint configurations
- [ ] Identify conditional flows and their conditions
- [ ] Note any shared flows referenced across proxies

**Policies:**

- [ ] Catalog every policy by type (security, mediation, traffic management)
- [ ] Document policy execution order in PreFlow, conditional flows, and
      PostFlow
- [ ] Note any custom JavaScript or Java callouts
- [ ] Identify policies with environment-specific configurations

**External dependencies:**

- [ ] List all target servers and their configurations
- [ ] Document key-value map entries and their usage
- [ ] Identify service callout endpoints
- [ ] Note any encrypted KVM entries (secrets)

**Developer ecosystem:**

- [ ] Count active developer apps and API keys
- [ ] Document API product configurations and quota limits
- [ ] Export developer portal customizations and custom pages
- [ ] List custom attributes on developers and apps

### Phase 2: Set Up Zuplo

1. **Create your Zuplo project** — sign up at
   [portal.zuplo.com](https://portal.zuplo.com) and create a new project
2. **Connect to Git** — link your project to a GitHub repository for
   [GitOps-based deployments](https://zuplo.com/docs/articles/source-control)
3. **Import your OpenAPI specs** — use the
   [OpenAPI import](https://zuplo.com/docs/articles/openapi) to bootstrap your
   route configuration
4. **Configure policies** — add authentication, rate limiting, and
   transformation policies to each route using the policy mapping table above
5. **Add custom logic** — rewrite any JavaScript or Java callouts as TypeScript
   modules in the `modules/` directory
6. **Set environment variables** — migrate Apigee KVM entries and target server
   URLs to Zuplo environment variables

### Phase 3: Parallel Running

Run Zuplo alongside Apigee to validate behavior before cutting over:

1. **Deploy to a preview environment** — every Git branch gets its own isolated
   Zuplo environment with a unique URL
2. **Run your test suite against Zuplo** — point integration tests at the
   preview URL
3. **Compare responses** — verify that Zuplo returns identical responses for the
   same requests
4. **Load test** — confirm that Zuplo handles your peak traffic patterns

### Phase 4: Traffic Cutover

Use one of these strategies for a zero-downtime cutover:

**DNS-based blue-green deployment:**

1. Lower your API domain's DNS TTL to 60 seconds (24 hours before cutover)
2. Switch DNS to point at Zuplo's edge network
3. Monitor error rates and latency
4. Roll back by reverting DNS if anything goes wrong

**Canary routing (percentage-based):**

1. Route 5% of traffic to Zuplo while 95% continues through Apigee
2. Monitor for errors and latency differences
3. Gradually increase to 25%, then 50%, then 100%
4. This approach gives you fine-grained control and easy rollback

For more detail on zero-downtime strategies, see our guide to
[migrating from self-hosted to managed API gateways](/learning-center/migrate-self-hosted-to-managed-api-gateway).

### Phase 5: Validation and Cleanup

After full cutover:

- [ ] Verify all routes return expected response codes
- [ ] Confirm authentication works for all methods
- [ ] Validate rate limiting triggers at correct thresholds
- [ ] Check that logs flow to your monitoring system
- [ ] Decommission Apigee proxies and infrastructure

## Handling Custom Apigee Policies

Custom logic is where most Apigee migrations stall. If you've invested in
JavaScript callouts, Java callouts, or Python scripts, you'll need to rewrite
them. The good news: TypeScript is a significant upgrade in every dimension.

### JavaScript Callouts → TypeScript Policies

Apigee JavaScript callouts use a proprietary runtime with Apigee-specific
objects (`context`, `proxyRequest`, `proxyResponse`). Zuplo uses standard Web
APIs — `Request`, `Response`, `Headers`, `fetch` — that any TypeScript developer
already knows.

**Apigee JavaScript callout:**

```javascript
// Apigee JavaScript policy
var apiKey = context.getVariable("request.header.x-api-key");
var tier = context.getVariable("apiproduct.developer.quota.limit");

if (tier === "premium") {
  context.setVariable("target.url", "https://premium-backend.example.com");
} else {
  context.setVariable("target.url", "https://standard-backend.example.com");
}
```

**Zuplo TypeScript equivalent:**

```typescript
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  const tier = request.user?.data?.tier;

  if (tier === "premium") {
    context.custom.backendUrl = environment.PREMIUM_BACKEND_URL;
  } else {
    context.custom.backendUrl = environment.STANDARD_BACKEND_URL;
  }

  return request;
}
```

### Java Callouts → TypeScript Modules

Java callouts are the heaviest lift in an Apigee migration. Apigee lets you
deploy compiled JAR files that run complex business logic — encryption,
third-party SDK integrations, data transformations.

In Zuplo, you replace Java callouts with TypeScript modules. For most use cases
this is straightforward — TypeScript has excellent ecosystem support for
cryptography (`crypto` Web API), HTTP clients (`fetch`), and data
transformation. For cases where you need a specific Java library, consider:

- **npm equivalents** — most Java libraries have TypeScript/JavaScript
  alternatives on npm
- **External microservice** — extract complex Java logic into a standalone
  service and call it via `fetch()` from your Zuplo policy
- **WebAssembly** — for compute-intensive operations, compile your Java code (or
  rewrite in Rust/Go) to Wasm

### Shared Flows → Composite Policies

Apigee's shared flows let you define reusable policy sequences that can be
called from multiple API proxies via FlowCallout. Zuplo's equivalent is
**composite policies** — group multiple policies into a single named policy that
can be attached to any route:

```json
{
  "name": "standard-security",
  "policyType": "composite-inbound",
  "handler": {
    "export": "CompositeInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "policies": ["api-key-auth", "rate-limit", "request-validation"]
    }
  }
}
```

This lets you define your "standard security stack" once and apply it
consistently across all routes, just like shared flows in Apigee.

## Analytics and Monitoring Migration

Apigee includes built-in analytics with dashboards for traffic, error rates,
latency, and developer engagement. Migrating your observability setup requires
understanding Zuplo's analytics model and integrating with your existing
monitoring stack.

### Built-in Analytics

Zuplo provides built-in analytics dashboards covering request volumes, error
rates, latency percentiles, and per-API-key usage. For most teams, this provides
immediate visibility without additional configuration.

### Integrating with Your Existing Monitoring Stack

If you're using third-party monitoring tools with Apigee, Zuplo supports direct
integrations through logging and metrics plugins:

- **Datadog** — stream logs and metrics directly from Zuplo
- **New Relic** — real-time log and metrics forwarding
- **Google Cloud Logging** — if you're keeping some GCP infrastructure, Zuplo
  integrates directly
- **Splunk** — log streaming for enterprise SIEM workflows
- **Dynatrace** — full observability integration
- **OpenTelemetry** — export traces and logs to any OTLP-compatible backend
  (Honeycomb, Jaeger, and more)

These integrations are configured in your `zuplo.runtime.ts` file — a TypeScript
module that runs at startup:

```typescript
import { RuntimeExtensions, environment } from "@zuplo/runtime";
import { DataDogLoggingPlugin } from "@zuplo/runtime";

export function runtimeInit(runtime: RuntimeExtensions) {
  runtime.addPlugin(
    new DataDogLoggingPlugin({
      apiKey: environment.DATADOG_API_KEY,
      url: "https://http-intake.logs.datadoghq.com/api/v2/logs",
    }),
  );
}
```

### Custom Logging

Every request in Zuplo is logged with structured data including the API key,
route, response code, latency, and request ID. You can add custom attributes
using `context.log` in any policy or handler:

```typescript
context.log.info("Order processed", {
  orderId: order.id,
  customerId: request.user?.sub,
  amount: order.total,
});
```

## Common Migration Pitfalls

### Forgetting About Shared Flows

**The mistake:** You migrate individual API proxies but forget that they depend
on shared flows for authentication, logging, or error handling.

**The fix:** Before migrating any proxy, trace its full execution path including
all FlowCallout references. Create composite policies in Zuplo that replicate
the shared flow behavior, then attach them to the appropriate routes.

### Overlooking Environment-Specific Configuration

**The mistake:** Apigee's environment configurations (target servers, KVM
entries, resource files) vary between dev, staging, and production. You migrate
the production config but forget the environment-specific differences.

**The fix:** Zuplo handles this with
[environment variables](https://zuplo.com/docs/articles/environment-variables)
scoped per environment type. Map each Apigee KVM entry and target server URL to
a Zuplo environment variable, and set different values for production vs.
preview environments.

### Ignoring Conditional Flow Logic

**The mistake:** Apigee's conditional flows execute different policy sequences
based on request attributes (path suffix, HTTP verb, headers). You migrate the
policies but not the conditions.

**The fix:** In Zuplo, conditional logic is handled either by defining separate
routes (each with its own policy set) or by implementing conditions inside a
custom code policy. Most Apigee conditional flows map naturally to separate
route definitions in your OpenAPI spec.

### Big-Bang Migration

**The mistake:** Migrating all API proxies at once on a single cutover date.

**The fix:** Migrate proxy by proxy, starting with low-traffic, low-risk
endpoints. Use canary routing to validate each batch before moving to the next.
This limits blast radius and gives you confidence with each step.

### Not Testing Under Real Load

**The mistake:** Your integration tests pass, so you assume production will
work.

**The fix:** Use shadow traffic testing or replay production access logs against
Zuplo. Synthetic tests don't catch the edge cases that real traffic exposes —
unusual header combinations, oversized payloads, concurrent requests from the
same client.

## Post-Migration Optimization

Once you've completed the migration, take advantage of Zuplo features that
Apigee didn't offer:

### Edge Deployment

Your APIs now run at 300+ edge locations globally. Requests are automatically
routed to the nearest point of presence, reducing latency for users everywhere —
not just in the GCP regions where Apigee was deployed.

### GitOps Workflow

Every change to your API gateway is a Git commit. Open a pull request and get an
isolated preview environment automatically. Review policy changes in code
review, just like application code. Roll back by reverting a commit. This is a
fundamentally better workflow than Apigee's UI-driven or script-based deployment
model.

### OpenAPI-First Development

Your `routes.oas.json` file is both your gateway configuration and your API
documentation source. Changes to routes automatically update your developer
portal. No more manually syncing documentation with behavior.

### API Monetization

If you're migrating monetized APIs, Zuplo offers
[built-in monetization](https://zuplo.com/docs/articles/monetization) with
usage-based billing integrations. Instead of building custom metering on top of
Apigee, you get native support for tracking usage, enforcing plan limits, and
integrating with billing platforms like Stripe.

### TypeScript Ecosystem

With Zuplo, your gateway logic is standard TypeScript. You can use any npm
package, share code between your API gateway and your application, and leverage
the full TypeScript ecosystem for testing, linting, and type checking.

## Next Steps

You now have a complete map from Apigee to Zuplo: every major policy has a
direct equivalent, the developer portal migration eliminates your Drupal
dependency, and the phased cutover approach keeps production traffic safe
throughout. The most common blockers — shared flows, environment-specific KVM
entries, and Java callouts — each have a clear TypeScript-based solution.

Migrating from Apigee is a significant project, but it pays dividends in reduced
complexity, better developer experience, and lower costs. The key is to approach
it methodically — inventory everything, map policies, migrate incrementally, and
validate thoroughly.

For a high-level comparison of Apigee and Zuplo features, see the
[Apigee alternative comparison page](/api-gateways/apigee-alternative-zuplo).
For guidance on migration strategies and zero-downtime patterns, see our guide
to
[migrating from self-hosted to managed API gateways](/learning-center/migrate-self-hosted-to-managed-api-gateway).

Ready to migrate? [Start a free Zuplo project](https://portal.zuplo.com/signup)
and import your first API proxy in minutes — no credit card required.