Skip to content

API proxy

Amy Farrell edited this page Jan 22, 2025 · 5 revisions

Part of USAGov Architecture patterns

In order to use data from external web services "on the fly," we'll need a server to handle the connection to the external service.

This is a proposal for a service we have not yet implemented.

  • Audience: general public (via client-side javascript)
    • API requests may be user-specific and on-demand
  • Purpose: Retrieve data from an API server for use in a web page or web application
    • Authenticate securely with the API server
    • Cache results (as appropriate for the external API)
    • Rate-limit requests to the external API
    • Support POST requests if the user's query should not be logged

Advantages

  • Can serve data that's updated in real time (dependent on the external service)
  • Server-to-server API access protects API credentials

Caveats

  • UX must account for possibility of the API Proxy or the external service being down (graceful degradation, distinguish "outage" from "no data")
  • More subject to overload or DDoS than a static site; consider caching responses where possible
  • Might use a separate domain from main public site so it doesn't share the same WAF
C4Context
  title API proxy
  Boundary(internet, "Internet", "the web") {
    Person(publicUser, "Public web user")
    System("externalAPI", "External API Server")
    Boundary(cloud_gov_boundary, "Cloud.gov", "") {
      Boundary(usagov_boundary, "USAgov org boundary", "") {
        Boundary(asg_restricted, "Restricted egress space", "") {
          ContainerDb(cache_storage, "Cache", "might be in-memory")
	  Container(api_proxy, "API proxy", "real-time updates", "Accesses External API, returns result", "")
          ContainerDb(credentials_service, "Credentials service", "Server's API creds")
	}
        Boundary(asg_public_egress, "Public egress space", "") {
	  Container(egress_proxy, "Egress proxy", "", "")
	}
      }
    }
  }
  Rel(publicUser, api_proxy, "HTTPS", "HEAD/GET/POST/(maybe more)")
  Rel(api_proxy, egress_proxy, "HTTPS", "HEAD/GET/*")
  Rel(api_proxy, cache_storage, "TLS", "read/write")
  Rel(api_proxy, credentials_service, "TLS", "read")
  Rel(egress_proxy, externalAPI, "HTTPS", "HEAD/GET/POST/(maybe more)")
  UpdateRelStyle(publicUser, api_proxy, $offsetX="-30", $offsetY="-120")
  UpdateRelStyle(api_proxy, egress_proxy, $offsetX="-20", $offsetY="10")
  UpdateRelStyle(egress_proxy, externalAPI, $offsetX="220", $offsetY="120")
  UpdateRelStyle(api_proxy, cache_storage, $offsetX="-20", $offsetY="0")
  UpdateRelStyle(api_proxy, credentials_service, $offsetX="0", $offsetY="30")
  UpdateElementStyle(egress_proxy, $bgColor="yellow", $fontColor="black")

Loading

Example flows

In these examples, the Egress proxy is not shown; it must be configured to allow access to the External APIs.

Simplest possible flow

The API proxy applies credentials and passes the response back to the client:

sequenceDiagram
  Public web user->>API proxy: Query? (POST)
  API proxy->>External API Server: Query? (POST)
  External API Server->>API proxy: Response (DATA)
  API proxy->>Public web user: Response (DATA)
Loading

Combining results from multiple API calls

Happy-path example for Elected Officials tool using two external API services:

sequenceDiagram
  Public web user->>API proxy: Reps for ADDRESS? (POST)
  API proxy->>External API Server 1 (USPS): ADDRESS good? (POST)
  External API Server 1 (USPS)->>API proxy: GOOD
  API proxy->>External API Server 2 (Civic API): Reps for ADDRESS? (POST)
  External API Server 2 (Civic API)->>API proxy: "DATA: Rep, rep, rep, rep ..."
  API proxy->>Public web user: "DATA: Rep, rep, rep, rep ..."
Loading

The same flow, with caching added. Here, we get the same request twice in a brief period. It could be a legitimate user refreshing a page for some reason, or it could be part of a DDoS attack:

sequenceDiagram
  Public web user->>API proxy: Reps for ADDRESS? (POST)
  API proxy-->>Cache:hash(ADDRESS)?
  Cache-->>API proxy: MISS
  API proxy->>External API Server 1 (USPS): ADDRESS good? (POST)
  External API Server 1 (USPS)->>API proxy: GOOD
  API proxy->>External API Server 2 (Civic API): Reps for ADDRESS? (POST)
  External API Server 2 (Civic API)->>API proxy: "DATA: Rep, rep, rep, rep ..."
  API proxy-->>Cache: [hash(ADDRESS), "DATA: Rep, rep, rep, rep ..."]
  API proxy->>Public web user: "DATA: Rep, rep, rep, rep ..."
  Note over Public web user: time passes (less than n minutes)
  Public web user->>API proxy: Reps for ADDRESS? (POST)
  API proxy-->>Cache:hash(ADDRESS)?
  Cache-->>API proxy: HIT: "DATA: Rep, rep, rep, rep ..."
  API proxy->>Public web user: "DATA: Rep, rep, rep, rep ..."
Loading