Skip to content

    Apigee

    Building Apigee API Proxies: Flows, Endpoints, Policies

    Build an Apigee API proxy step by step: bundle structure, endpoints, flows, conditions, route rules, mediation policies and shared flows, with working XML.

    By Published 18 min read

    Share

    This lesson is part of Apigee API Management: Beginner to Advanced. Enroll for free, then sign in, to read it in full.

    What you'll learn: How an Apigee API proxy is put together, from the bundle on disk to the flows that run on each request. You will build a pass-through proxy, add a transformation, then add a conditional route, using real XML and curl commands.

    A proxy is the thing you actually build in Apigee. Everything else in the platform (products, keys, quotas, analytics) hangs off it. The good news is that a proxy is not mysterious code: it is a folder of small XML files that say "when a request arrives, do these things in this order, then call that backend". Once you can read those files, you can debug any Apigee API.

    The core idea

    Picture a conveyor belt at an airport. A bag (the request) enters at one end, passes a series of stations (policies), is routed to the right plane (the backend), and the process runs again in reverse for the bag coming back (the response). Some stations are always there (the x-ray scanner). Others are used only for certain bags (the oversize counter). The belt is the flow; the stations are policies; the sign that says which plane to load is the route rule.

    Apigee's proxy has exactly that shape, in two halves:

    • The ProxyEndpoint faces the client. It defines the base path clients call and the flows that run for the request and for the response going back.
    • The TargetEndpoint faces your backend. It defines where the backend lives and the flows that run just before calling it and just after it answers.

    Policies are small XML components (verify a key, rename a header, convert XML to JSON). You declare each policy once in its own file and then attach it to a flow as a step. Behaviour is the order of the steps.

    The proxy pipeline: four flow stages between the client and the backendClient to Proxy request; Proxy request to Target request (RouteRule); Target request to Backend service; Backend service to Target resp.; Target resp. to Proxy response; Proxy response to PostClientFlow (sent)ClientClientProxy request: Pre, conditional, PostProxy requestPre, conditional,PostTarget request: Pre, conditional, PostTarget requestPre, conditional,PostBackend serviceBackendserviceTarget resp.: Pre, conditional, PostTarget resp.Pre, conditional,PostProxy response: Pre, conditional, PostProxy responsePre, conditional,PostPostClientFlow: after the client has the responsePostClientFlowafter the clienthas the responseRouteRulesent

    How to read it: the top row is the request going out, the bottom row is the response coming back. Each box is a place where you can attach policies. The route rule sits between the proxy request and the target request: it chooses which TargetEndpoint runs, or none.

    Anatomy of a proxy bundle

    A proxy is deployed as a bundle: a directory named apiproxy that you can zip and import. The Apigee configuration reference describes this layout.

    flowchart TD R["apiproxy/"] --> D["orders-facade.xml: descriptor, names the proxy"] R --> PX["proxies/"] R --> TG["targets/"] R --> PO["policies/"] R --> RS["resources/"] PX --> PE["default.xml: ProxyEndpoint"] TG --> TE["default.xml: TargetEndpoint"] PO --> P1["AM-AddHeader.xml, EV-ParseId.xml, ..."] RS --> R1["scripts, JAR files, XSLT files"]

    What each part does:

    Path Contents
    apiproxy/<name>.xml The descriptor. A single <APIProxy name="..."> element. The name must be unique in the organization and may use only letters, digits, underscore and hyphen
    proxies/default.xml The ProxyEndpoint: base path, flows, route rules
    targets/default.xml The TargetEndpoint: the backend address and its flows
    policies/*.xml One file per policy, each with a unique name
    resources/ Scripts (JavaScript), JAR files (Java callouts), XSLT files used by policies

    A bundle can hold more than one ProxyEndpoint and several TargetEndpoints; the name default is just the convention for the first one. Because the whole proxy is text, it belongs in Git, which the CI/CD lesson builds on.

    The ProxyEndpoint

    The documented skeleton looks like this (the elements are shown empty so the structure is visible):

    <ProxyEndpoint name="default">
      <PreFlow/>
      <Flows/>
      <PostFlow/>
      <HTTPProxyConnection>
        <BasePath>/weather</BasePath>
        <Properties/>
      </HTTPProxyConnection>
      <FaultRules/>
      <DefaultFaultRule/>
      <RouteRule name="default">
        <TargetEndpoint>default</TargetEndpoint>
      </RouteRule>
    </ProxyEndpoint>

    Points to remember:

    • BasePath defaults to /. Clients call https://HOST/BASE_PATH/RESOURCE. A base path must be unique (the environments page states the rule per environment group, the configuration reference per environment), so two proxies cannot claim the same one on the same hostname. A wildcard segment such as /team/*/members is allowed, but the double wildcard /**/ is not.
    • The host name does not belong to the proxy in Apigee X. Host names are assigned to environment groups, and the environment group routes a request to an environment, where the base path then selects the proxy. If you inherit a bundle from Apigee Edge you may see VirtualHost elements; the reference sample for the ProxyEndpoint does not use them.
    • Properties is optional. For example, request.payload.parse.limit sets how large a payload the proxy will parse (the documented range is 10M to 30M, default 10M).
    • Fault rules handle errors. They are covered in the fault handling lesson.

    The TargetEndpoint

    <TargetEndpoint name="default">
      <PreFlow name="PreFlow"><Request/><Response/></PreFlow>
      <HTTPTargetConnection>
        <URL>https://mocktarget.apigee.net</URL>
      </HTTPTargetConnection>
    </TargetEndpoint>

    A target endpoint uses one connection type: HTTPTargetConnection (the usual case), LocalTargetConnection (call another proxy in the same organization and environment), or ScriptTarget. With a null route (below) there is no TargetEndpoint at all.

    Target servers and load balancing. Hard-coding a backend URL into the bundle means a different bundle for each environment. Instead, name a target server and let each environment define what that name means.

    <TargetEndpoint name="default">
      <HTTPTargetConnection>
        <LoadBalancer>
          <Algorithm>RoundRobin</Algorithm>
          <Server name="orders-1"/>
          <Server name="orders-2"/>
          <Server name="orders-fallback">
            <IsFallback>true</IsFallback>
          </Server>
          <MaxFailures>5</MaxFailures>
        </LoadBalancer>
        <Path>/api</Path>
      </HTTPTargetConnection>
    </TargetEndpoint>

    A target server is a named record (host, port, whether TLS is on) defined per environment; the same name can point to a test host in test and a production host in prod, while the bundle stays unchanged. The load balancer supports three algorithms: RoundRobin (the default), Weighted (each server needs a Weight) and LeastConnections. MaxFailures defaults to 0, which means Apigee never removes a server from rotation; the load balancing page says to set it above zero when you use a health monitor. One server can be marked as a fallback, used only when the others are unavailable. Path applies only when a LoadBalancer is present.

    Flows: what runs, and in what order

    Every segment of the pipeline (proxy request, target request, target response, proxy response) is made of three kinds of flow:

    • PreFlow always runs first. Use it for work that every call needs, such as verifying a credential.
    • Conditional flows run only when their Condition is true. Only one conditional flow runs per segment: Apigee tests them in order and runs the first whose condition is true, then skips the rest.
    • PostFlow always runs last in the segment. Use it for work that every call needs after the specific logic, such as adding a response header.

    The full order for one request, as documented, is: proxy request PreFlow, proxy request conditional flows, proxy request PostFlow, then the same three for the target request, then the target response, then the proxy response, and finally the optional PostClientFlow. PostClientFlow runs after the response has been sent to the client, and only a few policy types can be attached there: the flows page lists FlowCallout, MessageLogging and ServiceCallout, while the configuration reference mentions only MessageLogging, so stick to MessageLogging for final logging.

    Here is one request moving through that pipeline, with the policies we will build later.

    sequenceDiagram participant C as Client participant PE as ProxyEndpoint participant TE as TargetEndpoint participant B as Backend C->>PE: GET /legacy/v1/users/Dana PE->>PE: PreFlow request: set correlation id PE->>PE: Conditional flow GetGreeting: extract name PE->>PE: RouteRule GreetingRoute picks target greeting PE->>TE: forward request TE->>TE: PreFlow request: name to query parameter TE->>B: GET /user?user=Dana B-->>TE: 200, Hello Dana TE-->>PE: target response PE->>PE: PostFlow response: echo correlation id PE-->>C: 200 response

    How to read it: all the Apigee-side work happens in the steps between client and backend. The proxy shapes the request before it leaves and the response before it returns.

    A ProxyEndpoint flow section looks like this. A step is one policy name, optionally with its own condition.

    <Flows>
      <Flow name="GetGreeting">
        <Condition>(proxy.pathsuffix MatchesPath "/users/*") and (request.verb = "GET")</Condition>
        <Request>
          <Step><Name>EV-UserName</Name></Step>
        </Request>
        <Response/>
      </Flow>
    </Flows>

    Conditions

    A condition is an expression over flow variables: variable operator "value". Spaces around the operator are required. Combine tests with and, or and not, and group with parentheses. The documentation says Java operator precedence applies without listing the details, so add parentheses whenever you mix and with or.

    Operator Meaning Example
    = (or Equals) Equal, case-sensitive request.verb = "GET"
    != Not equal, case-sensitive request.verb != "OPTIONS"
    := Equal, case-insensitive request.header.accept := "application/json"
    Matches or ~ Glob-style match with *, case-sensitive request.header.user-agent Matches "*Mobile*"
    JavaRegex or ~~ Java regular expression proxy.pathsuffix JavaRegex "^/orders/[0-9]+
    quot;
    MatchesPath or ~/ URI path match proxy.pathsuffix MatchesPath "/orders/*"
    StartsWith Starts with, case-sensitive proxy.pathsuffix StartsWith "/admin"

    MatchesPath is the one to learn. In a path pattern, * matches one path segment and ** matches several levels. So /orders/* matches /orders/42 but not /orders/42/items, while /orders/** matches both. Matches is a plain glob over any string and is a common wrong choice for paths.

    Other behaviour to know:

    • Missing values. Compare against null to test for an absent variable: request.header.x-correlation-id = null is true when the header is not set. The pattern operators (Matches, JavaRegex, MatchesPath) return false when the left side is null.
    • Conditions also work on steps. A <Condition> inside a <Step> decides whether that one policy runs, for example only when response.status.code = 503.
    • Use proxy.pathsuffix for resource routing. It is the part of the client URL after the base path.

    Route rules

    Once the proxy request flows finish, Apigee evaluates the RouteRules to decide where the request goes. Rules are checked top to bottom and the first match wins. A rule without a condition always matches, so the unconditional default rule must be last.

    <RouteRule name="NoTarget">
      <Condition>proxy.pathsuffix MatchesPath "/summary"</Condition>
    </RouteRule>
    <RouteRule name="GreetingRoute">
      <Condition>proxy.pathsuffix MatchesPath "/users/*"</Condition>
      <TargetEndpoint>greeting</TargetEndpoint>
    </RouteRule>
    <RouteRule name="default">
      <TargetEndpoint>default</TargetEndpoint>
    </RouteRule>

    The first rule has no target. That is a null route: the request is not forwarded, and the proxy builds the response itself from policies. It is how you write a proxy that aggregates calls or returns a mock response. A RouteRule can also name a <URL> directly instead of a TargetEndpoint.

    Flow variables

    Flow variables are the proxy's shared memory. Apigee fills many of them (read-only facts about the message, the proxy and the caller) and you create your own. Policies read them in conditions and in {curly brace} templates inside XML. Some documented ones you will use constantly:

    Variable Meaning Writable?
    request.verb HTTP method No
    request.header.NAME A request header Yes
    request.queryparam.NAME A request query parameter Yes
    request.uri Base path plus the remaining path and query No
    proxy.basepath The ProxyEndpoint base path No
    proxy.pathsuffix The URL part after the base path No
    target.url The backend URL, set in the target request flow Yes
    target.copy.pathsuffix Whether the path suffix is forwarded to the target Yes
    response.status.code Response status code Yes
    route.name Name of the RouteRule that ran No
    messageid A globally unique id for the request No
    client.ip Address of the client (or of the system in front of Apigee) No

    Variables also have scope: proxy.pathsuffix is meaningful in the proxy request, target.url in the target request, and so on. A variable read outside its scope may be empty or unavailable. Custom variables are created by policies (<AssignVariable>, ExtractVariables, a ServiceCallout response) and last for the request.

    The mediation policies you will use most

    Mediation means changing a message in flight. These six policies cover most of it.

    Policy Job Watch out for
    AssignMessage Set, add, copy or remove headers, query parameters, payload, status code, verb; set variables; build a new message Without AssignTo it acts on the request in a request flow and on the response in a response flow
    ExtractVariables Pull values out of a path, header, query parameter or JSON/XML body into variables The variable name is prefix.name; without a prefix it is just the name in braces
    XMLToJSON and JSONToXML Convert the body format They apply only if the message Content-Type matches (application/xml for XMLToJSON, application/json for JSONToXML); otherwise the policy is skipped
    ServiceCallout Make an extra HTTP request from the proxy and keep the result in a variable Set a sensible Timeout; the default is 55,000 ms
    JavaScript Run a script for logic no other policy covers Keep to simple data work, set timeLimit, avoid system calls
    JavaCallout Run compiled Java from a JAR in resources/ Only when JavaScript or built-in policies are not enough

    Shared flows and flow hooks

    A shared flow is a reusable list of policy steps with no endpoint of its own. Proxies (and other shared flows) run it through a FlowCallout policy. Its bundle layout mirrors a proxy:

    sharedflowbundle/
      security-baseline.xml        descriptor: <SharedFlowBundle name="security-baseline">
      sharedflows/default.xml      <SharedFlow name="default"> with <Step> elements
      policies/                    the policy files the steps name
      resources/                   scripts, JARs, XSLT

    Calling it from a proxy:

    <FlowCallout name="FC-SecurityBaseline">
      <SharedFlowBundle>security-baseline</SharedFlowBundle>
    </FlowCallout>

    A bundle holds one shared flow, and the callout fails with flow.SharedFlowNotFound (HTTP 500) if the named shared flow does not exist or is not deployed. A flow hook attaches a shared flow to a fixed point for every proxy in an environment: PreProxyFlowHook (before the proxy endpoint runs), PreTargetFlowHook (before the target is called), PostTargetFlowHook (after the target answers) and PostProxyFlowHook (after the proxy endpoint, before the response is sent). Each hook accepts one shared flow. Because the effect is environment-wide, hooks suit organisation policy (a security baseline, response logging) and are a poor place for anything one team owns.

    How to implement it: build three proxies' worth of behaviour in one

    You will build one proxy called legacy-facade in four steps. The backend is Apigee's public mock target, https://mocktarget.apigee.net, which stands in for a legacy service. Its documented endpoints include /json and /xml (sample data), /echo (returns the request headers and body as JSON) and /user?user=NAME (a greeting).

    Placeholders: ORG is your Apigee organization, ENV an environment such as test, and api.example.com stands for a host name on your environment group.

    Step 1: a pass-through proxy

    Create this tree:

    legacy-facade/
      apiproxy/
        legacy-facade.xml
        proxies/default.xml
        targets/default.xml

    apiproxy/legacy-facade.xml:

    <APIProxy name="legacy-facade"/>

    apiproxy/proxies/default.xml:

    <ProxyEndpoint name="default">
      <HTTPProxyConnection>
        <BasePath>/legacy/v1</BasePath>
      </HTTPProxyConnection>
      <RouteRule name="default">
        <TargetEndpoint>default</TargetEndpoint>
      </RouteRule>
    </ProxyEndpoint>

    apiproxy/targets/default.xml:

    <TargetEndpoint name="default">
      <HTTPTargetConnection>
        <URL>https://mocktarget.apigee.net</URL>
      </HTTPTargetConnection>
    </TargetEndpoint>

    Import and deploy with the Apigee API. The import call takes the zipped bundle as a multipart file field; it creates the proxy, or a new revision if it exists. Deploying with override=true makes the new revision replace whichever revision is deployed in that environment.

    export ORG=my-apigee-org ENV=test
    export TOKEN=$(gcloud auth print-access-token)
    export API=https://apigee.googleapis.com/v1/organizations/$ORG
    
    (cd legacy-facade && zip -r ../legacy-facade.zip apiproxy)
    
    # Optional: validate the bundle without importing it
    curl -s -X POST "$API/apis?action=validate&name=legacy-facade" \
      -H "Authorization: Bearer $TOKEN" -F "file=@legacy-facade.zip"
    
    # Import (the response includes the new revision number)
    REV=$(curl -s -X POST "$API/apis?action=import&name=legacy-facade" \
      -H "Authorization: Bearer $TOKEN" -F "file=@legacy-facade.zip" | jq -r '.revision')
    
    # Deploy that revision to the environment
    curl -s -X POST "$API/environments/$ENV/apis/legacy-facade/revisions/$REV/deployments?override=true" \
      -H "Authorization: Bearer $TOKEN"

    Try it (the first call returns the mock target's sample JSON unchanged; the second shows the request headers it received):

    curl -i https://api.example.com/legacy/v1/json
    curl -s https://api.example.com/legacy/v1/echo -H "X-Demo: hello"

    The path after /legacy/v1 is forwarded to the target, so /legacy/v1/json reaches https://mocktarget.apigee.net/json. If something goes wrong, open the Debug tool in the console, start a session on this proxy and repeat the call: it shows every flow, step, condition result and variable.

    Step 2: add a transformation

    The mock target returns XML at /xml. Convert it to JSON on the way back. Create apiproxy/policies/XMLToJSON-Body.xml, using the documented in-place pattern (source and output are both the response):

    <XMLToJSON name="XMLToJSON-Body">
      <Options/>
      <OutputVariable>response</OutputVariable>
      <Source>response</Source>
    </XMLToJSON>

    Attach it with a conditional flow in proxies/default.xml, placed before the route rules:

    <Flows>
      <Flow name="XmlAsJson">
        <Condition>(proxy.pathsuffix MatchesPath "/xml") and (request.verb = "GET")</Condition>
        <Request/>
        <Response>
          <Step><Name>XMLToJSON-Body</Name></Step>
        </Response>
      </Flow>
    </Flows>

    Re-zip, import and deploy as before, then call https://api.example.com/legacy/v1/xml. The same four fields (city, first name, last name, state) now come back as JSON. The exact shape, such as whether they sit under a root key, depends on the policy options. If the response is still XML, check that the backend sets Content-Type: application/xml, because the policy is skipped otherwise.

    The reverse works with JSONToXML on the request flow, with <Source>request</Source> and <OutputVariable>request</OutputVariable>. It needs Content-Type: application/json on the request.

    Step 3: add a conditional route and a REST-to-legacy mapping

    Suppose the legacy service wants /user?user=NAME, but you want clients to call /users/NAME. Do the mapping in three parts: extract the name, route to a second target, rewrite the request.

    policies/EV-UserName.xml reads the name out of the path:

    <ExtractVariables name="EV-UserName">
      <Source>request</Source>
      <URIPath>
        <Pattern ignoreCase="true">/users/{name}</Pattern>
      </URIPath>
      <VariablePrefix>path</VariablePrefix>
      <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    </ExtractVariables>

    For /users/Dana this sets path.name to Dana.

    policies/AM-NameToQuery.xml builds the backend query and stops the path suffix being copied to the target:

    <AssignMessage name="AM-NameToQuery">
      <AssignTo>request</AssignTo>
      <Set>
        <QueryParams>
          <QueryParam name="user">{path.name}</QueryParam>
        </QueryParams>
      </Set>
      <AssignVariable>
        <Name>target.copy.pathsuffix</Name>
        <Value>false</Value>
      </AssignVariable>
    </AssignMessage>

    A second target, targets/greeting.xml, points at the legacy path. Its PreFlow runs the rewrite:

    <TargetEndpoint name="greeting">
      <PreFlow name="PreFlow">
        <Request>
          <Step><Name>AM-NameToQuery</Name></Step>
        </Request>
        <Response/>
      </PreFlow>
      <HTTPTargetConnection>
        <URL>https://mocktarget.apigee.net/user</URL>
      </HTTPTargetConnection>
    </TargetEndpoint>

    Add the conditional flow that extracts the name, and the conditional route. Rules are ordered, with default last:

    <Flow name="GetGreeting">
      <Condition>(proxy.pathsuffix MatchesPath "/users/*") and (request.verb = "GET")</Condition>
      <Request>
        <Step><Name>EV-UserName</Name></Step>
      </Request>
      <Response/>
    </Flow>
    <RouteRule name="GreetingRoute">
      <Condition>proxy.pathsuffix MatchesPath "/users/*"</Condition>
      <TargetEndpoint>greeting</TargetEndpoint>
    </RouteRule>
    <RouteRule name="default">
      <TargetEndpoint>default</TargetEndpoint>
    </RouteRule>

    Test: curl https://api.example.com/legacy/v1/users/Dana should return Hello, Dana!, while /legacy/v1/json still takes the default route. The decision this step teaches is where each piece of logic belongs. Choosing the target is a route rule matter. Reshaping the request for one target belongs in that target's own flow. Pulling data out of the incoming call is a proxy request flow matter.

    Step 4: use cases with real policies

    The first use case (a REST facade over a legacy backend) is what you just built. Two more.

    Use case 2: aggregation with ServiceCallout. One client call fans out to two backends and returns a combined result. This is a null route: the proxy answers itself. The two callouts hit the mock target's /json and /ip endpoints as stand-ins for a customer service and a network service.

    policies/SC-Profile.xml (the second callout, SC-Network, is the same with another variable name and URL https://mocktarget.apigee.net/ip):

    <ServiceCallout name="SC-Profile">
      <Request clearPayload="true">
        <Set>
          <Verb>GET</Verb>
        </Set>
      </Request>
      <Response>profileResponse</Response>
      <Timeout>5000</Timeout>
      <HTTPTargetConnection>
        <URL>https://mocktarget.apigee.net/json</URL>
      </HTTPTargetConnection>
    </ServiceCallout>

    policies/EV-Profile.xml reads from the callout response variable (and EV-Network does the same for $.ip with prefix net):

    <ExtractVariables name="EV-Profile">
      <Source>profileResponse</Source>
      <JSONPayload>
        <Variable name="firstName"><JSONPath>$.firstName</JSONPath></Variable>
        <Variable name="lastName"><JSONPath>$.lastName</JSONPath></Variable>
        <Variable name="city"><JSONPath>$.city</JSONPath></Variable>
      </JSONPayload>
      <VariablePrefix>profile</VariablePrefix>
    </ExtractVariables>

    policies/AM-BuildSummary.xml writes the combined response:

    <AssignMessage name="AM-BuildSummary">
      <AssignTo>response</AssignTo>
      <Set>
        <Payload contentType="application/json">{"customer":"{profile.firstName} {profile.lastName}","city":"{profile.city}","seenFrom":"{net.ip}"}</Payload>
        <StatusCode>200</StatusCode>
      </Set>
    </AssignMessage>

    The flow runs the callouts in the request section and builds the response in the response section; the NoTarget route from the route-rule section sends nothing to a backend:

    <Flow name="Summary">
      <Condition>(proxy.pathsuffix MatchesPath "/summary") and (request.verb = "GET")</Condition>
      <Request>
        <Step><Name>SC-Profile</Name></Step>
        <Step><Name>EV-Profile</Name></Step>
        <Step><Name>SC-Network</Name></Step>
        <Step><Name>EV-Network</Name></Step>
      </Request>
      <Response>
        <Step><Name>AM-BuildSummary</Name></Step>
      </Response>
    </Flow>

    The two callouts run one after the other, so latency adds up; the Timeout bounds the damage. The routes page documents the null route but gives no example of building the response in it, so treat this AssignMessage as an outline and confirm the result with a Debug session before relying on it.

    Use case 3: header normalisation and a correlation id. Every request should carry an X-Correlation-Id that follows it to the backend and back, using the client's value if present, otherwise a generated one. Two policies, each guarded by a step condition on whether the header exists:

    <AssignMessage name="AM-CorrelationIdKeep">
      <AssignTo>request</AssignTo>
      <AssignVariable>
        <Name>corr.id</Name>
        <Ref>request.header.x-correlation-id</Ref>
      </AssignVariable>
    </AssignMessage>
    <AssignMessage name="AM-CorrelationIdGenerate">
      <AssignTo>request</AssignTo>
      <Set>
        <Headers>
          <Header name="X-Correlation-Id">{messageid}</Header>
        </Headers>
      </Set>
      <AssignVariable>
        <Name>corr.id</Name>
        <Template>{messageid}</Template>
      </AssignVariable>
    </AssignMessage>
    <AssignMessage name="AM-CorrelationIdResponse">
      <AssignTo>response</AssignTo>
      <Set>
        <Headers>
          <Header name="X-Correlation-Id">{corr.id}</Header>
        </Headers>
      </Set>
    </AssignMessage>

    Attach the first two in the ProxyEndpoint PreFlow with opposite step conditions, and the third in the PostFlow response. The complete ProxyEndpoint, with everything built so far:

    <ProxyEndpoint name="default">
      <PreFlow name="PreFlow">
        <Request>
          <Step>
            <Name>AM-CorrelationIdKeep</Name>
            <Condition>request.header.x-correlation-id != null</Condition>
          </Step>
          <Step>
            <Name>AM-CorrelationIdGenerate</Name>
            <Condition>request.header.x-correlation-id = null</Condition>
          </Step>
        </Request>
        <Response/>
      </PreFlow>
      <Flows>
        <Flow name="XmlAsJson">
          <Condition>(proxy.pathsuffix MatchesPath "/xml") and (request.verb = "GET")</Condition>
          <Request/>
          <Response>
            <Step><Name>XMLToJSON-Body</Name></Step>
          </Response>
        </Flow>
        <Flow name="GetGreeting">
          <Condition>(proxy.pathsuffix MatchesPath "/users/*") and (request.verb = "GET")</Condition>
          <Request>
            <Step><Name>EV-UserName</Name></Step>
          </Request>
          <Response/>
        </Flow>
        <Flow name="Summary">
          <Condition>(proxy.pathsuffix MatchesPath "/summary") and (request.verb = "GET")</Condition>
          <Request>
            <Step><Name>SC-Profile</Name></Step>
            <Step><Name>EV-Profile</Name></Step>
            <Step><Name>SC-Network</Name></Step>
            <Step><Name>EV-Network</Name></Step>
          </Request>
          <Response>
            <Step><Name>AM-BuildSummary</Name></Step>
          </Response>
        </Flow>
      </Flows>
      <PostFlow name="PostFlow">
        <Request/>
        <Response>
          <Step><Name>AM-CorrelationIdResponse</Name></Step>
        </Response>
      </PostFlow>
      <HTTPProxyConnection>
        <BasePath>/legacy/v1</BasePath>
      </HTTPProxyConnection>
      <RouteRule name="NoTarget">
        <Condition>proxy.pathsuffix MatchesPath "/summary"</Condition>
      </RouteRule>
      <RouteRule name="GreetingRoute">
        <Condition>proxy.pathsuffix MatchesPath "/users/*"</Condition>
        <TargetEndpoint>greeting</TargetEndpoint>
      </RouteRule>
      <RouteRule name="default">
        <TargetEndpoint>default</TargetEndpoint>
      </RouteRule>
    </ProxyEndpoint>

    Test: curl -i https://api.example.com/legacy/v1/echo -H "X-Correlation-Id: abc-123" should echo abc-123 in both the body (the mock target reflects request headers) and the response header. Without the header, the value is a generated id.

    Choosing between ServiceCallout, JavaScript, a shared flow and the built-ins

    Reach for the simplest tool that does the job. The sequence of questions below is the decision.

    flowchart TD Q1{"Does a built-in policy already do it? AssignMessage, ExtractVariables, XMLToJSON, Quota..."} -->|"Yes"| A1["Use that policy"] Q1 -->|"No"| Q2{"Do you need data from another HTTP service?"} Q2 -->|"Yes"| Q3{"Is it another proxy in the same org and environment?"} Q3 -->|"Yes"| A2["ServiceCallout with LocalTargetConnection"] Q3 -->|"No"| A3["ServiceCallout to the URL, then ExtractVariables"] Q2 -->|"No"| Q4{"Is the same set of steps needed by many proxies?"} Q4 -->|"Yes"| A4["Shared flow called with FlowCallout, or a flow hook for every proxy"] Q4 -->|"No"| Q5{"Small data massaging, a few lines?"} Q5 -->|"Yes"| A5["JavaScript policy with a timeLimit"] Q5 -->|"No"| A6["JavaCallout, or move the logic into a backend service"]

    How to read it: logic that is heavy, stateful or domain-specific has no good home in a gateway. The last box is deliberate, because the right answer is often to move it into a service.

    Need Best fit Cost
    Add or rename a header, change a status code AssignMessage None
    Combine two backend calls ServiceCallout plus ExtractVariables Extra latency per call, a new failure mode
    Same security checks on every proxy Shared flow plus flow hook Environment-wide blast radius
    Tricky string or JSON logic JavaScript CPU time, harder to test, a time limit
    Existing compiled library JavaCallout JAR lifecycle to manage

    In practice: fronting a legacy customer service

    NOTE

    A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.

    The situation

    A bank's mobile team needs customer profile data held in a legacy service that returns XML, takes identifiers as query parameters (/customer?id=42) and has no correlation logging. Three mobile releases are in the field (assumed). The legacy team will not change the service this year, and support cannot trace a slow call across the app, the gateway and the service.

    Starting architecture

    Starting point: mobile apps call the legacy service and parse XML themselvesMobile app to Load balancer (XML); Load balancer to Legacy service; Legacy service to Service logsMobile app: three releasesMobile appthree releasesLoad balancerLoad balancerLegacy service: XML, query parametersLegacy serviceXML, queryparametersService logs: no correlation idService logsno correlation idXML

    How to read it: the app carries the legacy service's format quirks, and nothing links an app request to a log line.

    What changes

    1. Create a proxy with base path /customers/v1 and the legacy service as a target server named legacy-customers, defined per environment so test and production use different hosts.
    2. In the ProxyEndpoint PreFlow, add the correlation id policies from this lesson (keep a client id, otherwise generate one).
    3. Add a conditional flow GetCustomer for GET /customers/*. It extracts the id from the path, and a request rewrite in the target flow turns it into ?id=... with target.copy.pathsuffix set to false.
    4. Add XMLToJSON to the proxy response flow of that conditional flow, so the apps receive JSON.
    5. Add an /summary style flow that calls the customer and accounts services with ServiceCallout and returns one combined response, so the app makes one round trip.
    6. Put the common steps (correlation id, standard headers) in a shared flow so later proxies reuse them.
    After: a facade proxy hides the legacy format and stamps a correlation idMobile app to Apigee proxy (REST, JSON); Apigee proxy to Shared flow (FlowCallout); Apigee proxy to Legacy service (query rewrite); Apigee proxy to Accounts service (ServiceCallout)Mobile app: JSON onlyMobile appJSON onlyApigee proxy: customers/v1 facadeApigee proxycustomers/v1facadeShared flow: correlation id, headersShared flowcorrelation id,headersLegacy service: unchanged, target serverLegacy serviceunchanged, targetserverAccounts service: second calloutAccountsservicesecond calloutREST, JSONFlowCalloutquery rewriteServiceCallout

    Result

    What the team should see: apps receive JSON with the same contract across releases, the legacy service is untouched, and a single correlation id appears in the app logs, the Apigee Debug session and the service logs, so support can follow one request end to end. Adding a second backend host is a target server change, not a code change. When something misbehaves, the Debug session shows which flow ran, which condition matched and which policy changed a variable.

    Architect's notes

    • Keep the proxy thin. Routing, security, shaping and policy belong here; business rules belong in services. If a flow grows past a dozen steps or needs loops, move the logic out.
    • Prefer built-in policies over scripts. Declarative XML is easier to review and test than JavaScript, and it has no time limit to tune.
    • Name policies with a prefix and a verb (AM-, EV-, SC-, RF-) so a Debug trace is readable at a glance, and keep one concern per policy.
    • Budget latency. Every ServiceCallout adds a network hop and a failure mode. Set timeouts well under the client's own timeout, and decide what the proxy returns when an optional callout fails.
    • Externalise every environment difference into target servers and, where needed, key value maps, never into the bundle.
    • Flow hooks are a sharp tool. They run for every proxy in the environment, so test a change on a non-production environment first, and keep one owner.
    • Version through base paths. Plan how /v1 and /v2 coexist before the first client depends on /v1 (see API design, products and developer apps).

    Common mistakes

    • Putting the default RouteRule first. An unconditional rule matches everything, so the conditional rules below it never run. Put default last.
    • Using Matches for paths. Matches is a string glob; use MatchesPath with * for one segment and ** for several.
    • Expecting every matching conditional flow to run. Only the first matching conditional flow per segment runs. Put the more specific condition first, or fold shared work into the PreFlow.
    • One giant PreFlow. Resource-specific logic in the PreFlow runs for every call. Use conditional flows per resource and keep the PreFlow for cross-cutting work.
    • Mixing and and or without parentheses. Add the parentheses; do not rely on precedence.
    • Converting a body whose Content-Type is wrong. XMLToJSON and JSONToXML are skipped silently if the header does not match. Check the header in Debug, and set it first if needed.
    • A ServiceCallout with no Response element. The proxy does not wait for a reply and cannot see a failure. Name a response variable and check it.
    • Leaving the callout timeout at the default. The default is 55 seconds. Set a short explicit Timeout.
    • Hard-coded backend URLs. The same bundle cannot be promoted between environments. Use target servers.
    • Calling a shared flow that is not deployed. The proxy fails at run time with a 500 (flow.SharedFlowNotFound). Deploy shared flows first and monitor them as part of the release.
    • Putting ordinary policies in PostClientFlow. Only a few policy types are allowed there; use it for MessageLogging.

    Key takeaways

    • A proxy is a bundle of XML: a descriptor, a ProxyEndpoint, one or more TargetEndpoints, policy files and optional resources.
    • The ProxyEndpoint faces the client (base path, flows, route rules); the TargetEndpoint faces the backend (URL or target servers, flows).
    • Each segment has a PreFlow, conditional flows and a PostFlow, and only the first matching conditional flow runs.
    • Conditions test flow variables; use MatchesPath for paths and parentheses for mixed logic.
    • Route rules run top to bottom, first match wins, default last; a rule with no target is a null route.
    • AssignMessage, ExtractVariables, XMLToJSON, JSONToXML, ServiceCallout and JavaScript cover most mediation needs, in that order of preference.
    • Shared flows reuse steps via FlowCallout; flow hooks apply one to every proxy in an environment.
    • Build incrementally and use the Debug tool: pass-through first, then one change at a time.

    Practice exercises

    Starter (about 30 min): Build and deploy the pass-through legacy-facade proxy against the mock target. Call /json and /echo. Then add the XMLToJSON flow and confirm /xml returns JSON. Run a Debug session and note which flow and step ran.

    Intermediate (about 2 h): Add the greeting facade (/users/NAME mapped to /user?user=NAME) with the conditional route, a second target and the rewrite policy. Add the correlation id policies and prove, using /echo, that a client-supplied id is preserved and a missing one is generated. Then change the proxy so /users/NAME with a verb other than GET returns a 405 using a RaiseFault policy.

    Advanced (half a day): Create the /summary aggregation with two ServiceCallouts, then move the correlation id policies into a shared flow called through FlowCallout. Replace the hard-coded mock URL with a target server named mock-backend that you create in two environments pointing at different hosts, and promote the same zipped bundle through both. Write down what happens to the aggregation when the second callout times out, then decide and implement the behaviour you want.

    Official documentation

    Enroll to keep reading

    Lesson 4 of 11 · Apigee API Management: Beginner to Advanced

    Build an Apigee API proxy step by step: bundle structure, endpoints, flows, conditions, route rules, mediation policies and shared flows, with working XML.

    In the rest of this lesson

    • Flow variables
    • The mediation policies you will use most
    • How to implement it: build three proxies' worth of behaviour in one
    • Choosing between ServiceCallout, JavaScript, a shared flow and the built-ins
    • In practice: fronting a legacy customer service
    • Architect's notes

    Enrolling is free. It unlocks every lesson in Apigee API Management: Beginner to Advanced, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments