---
title: "CI/CD for API Gateways: Pipeline Templates and Multi-Environment Deployment"
description: "Set up CI/CD pipelines for your API gateway with GitHub Actions and GitLab CI templates, multi-environment deployment, branch previews, and rollback strategies."
canonicalUrl: "https://zuplo.com/learning-center/ci-cd-api-gateway-deployment"
pageType: "learning-center"
authors: "nate"
tags: "API Gateway, API Best Practices"
image: "https://zuplo.com/og?text=CI%2FCD%20for%20API%20Gateways%3A%20Pipeline%20Templates%20and%20Multi-Environment%20Deployment"
---
- [Introduction](#introduction)
- [Why CI/CD for API Gateways Matters](#why-cicd-for-api-gateways-matters)
- [Pipeline Architecture](#pipeline-architecture)
- [GitHub Actions Template](#github-actions-template)
- [GitLab CI Template](#gitlab-ci-template)
- [Multi-Environment Deployment](#multi-environment-deployment)
- [Branch Preview Environments](#branch-preview-environments)
- [Multi-Region Deployment](#multi-region-deployment)
- [Rollback Strategies](#rollback-strategies)
- [Testing in the Pipeline](#testing-in-the-pipeline)
- [Get Started with Zuplo](#get-started-with-zuplo)

## Introduction

API gateway configuration has traditionally lived outside of version control.
Teams log into admin dashboards, click through forms, toggle settings, and hope
that staging matches production. When something breaks, the question is always
the same: "Who changed what, and when?"

This is the exact problem CI/CD solves for application code, and there is no
reason your API gateway should be different. Your routes, policies, rate limits,
and authentication rules are configuration that deserves the same rigor as your
source code: version-controlled, peer-reviewed, automatically tested, and
deployed through a repeatable pipeline.

Modern API gateways like [Zuplo](https://zuplo.com) are built around this
principle. Zuplo is [git-native](/learning-center/what-is-gitops) by design,
meaning your gateway configuration lives in a Git repository and deploys through
standard CI/CD workflows. Even the Zuplo portal syncs with Git, so there is no
configuration that can drift out of sync with reality. The repo is the source of
truth.

In this guide, you will set up complete CI/CD pipelines for API gateway
deployment using GitHub Actions and GitLab CI. You will learn how to manage
multiple environments, automate preview deployments for pull requests, deploy
across regions, and implement rollback strategies that keep your APIs reliable.

## Why CI/CD for API Gateways Matters

Deploying API gateway changes manually introduces the same risks as deploying
application code manually: inconsistency, human error, and a lack of
accountability. CI/CD pipelines eliminate these risks and bring several concrete
benefits.

### Consistency Across Environments

When your gateway configuration deploys through a pipeline, every environment
gets exactly the same configuration, transformed only by environment-specific
variables. There is no "I forgot to update staging" or "production has a
different rate limit than what we tested." The pipeline enforces parity.

### Audit Trail

Every change to your API gateway is a Git commit. You can see who changed what,
when they changed it, and why (through commit messages and PR descriptions).
This is not just good practice; it is a compliance requirement for many
organizations operating in regulated industries.

### Automated Testing

A pipeline can validate your OpenAPI specification, run contract tests against
preview environments, and verify that rate-limiting policies behave as expected
before a single request hits production. Manual processes cannot match this
level of consistency.

### Rollback Capability

When your gateway configuration is in Git, rolling back is as simple as
reverting a commit and letting the pipeline redeploy. No one needs to remember
what the previous dashboard settings were.

### Team Collaboration Through Pull Requests

Pull requests give your team a structured way to propose, review, and approve
API changes. A new route, a modified authentication policy, or a rate limit
adjustment all go through the same review process as any other code change.

## Pipeline Architecture

A well-structured API gateway CI/CD pipeline follows a predictable flow that
balances speed with safety.

```
Commit --> Pull Request --> Preview Deploy --> Review & Test
                                                   |
                                              Merge to main
                                                   |
                                           Staging Deploy --> Integration Tests
                                                   |
                                          Production Deploy
```

Each stage serves a specific purpose:

1. **Commit** -- The developer pushes gateway configuration changes to a feature
   branch.
2. **Pull Request** -- A PR triggers validation checks: OpenAPI linting, schema
   validation, and policy checks.
3. **Preview Deploy** -- The pipeline deploys the changes to an isolated preview
   environment where reviewers and automated tests can verify behavior against a
   live gateway.
4. **Merge to main** -- After approval and passing checks, the PR merges into
   the main branch.
5. **Staging Deploy** -- The merge triggers a deployment to the staging
   environment for final integration testing.
6. **Production Deploy** -- After staging validation passes, the pipeline
   deploys to production.

This architecture ensures that no change reaches production without being
validated at multiple stages. Let's implement this with real pipeline templates.

## GitHub Actions Template

The following GitHub Actions workflow handles the complete lifecycle: validation
on pull requests, preview deployments, and production deployment on merge.

### Workflow File

Create `.github/workflows/api-gateway-deploy.yml`:

```yaml
name: API Gateway Deploy

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

env:
  ZUPLO_API_KEY: ${{ secrets.ZUPLO_API_KEY }}

jobs:
  validate:
    name: Validate OpenAPI
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Lint OpenAPI specification
        run: npx @redocly/cli lint openapi.json

      - name: Validate gateway configuration
        run: |
          npx zuplo dev &
          sleep 10
          npx zuplo test --endpoint http://localhost:9000
          kill %1

  deploy-preview:
    name: Deploy Preview
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    needs: validate
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Deploy to preview environment
        id: preview
        run: |
          OUTPUT=$(npx zuplo deploy --api-key "$ZUPLO_API_KEY" 2>&1)
          echo "$OUTPUT"
          DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oP 'Deployed to \K(https://[^ ]+)')
          echo "url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"

      - name: Comment preview URL on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Preview environment deployed: ${{ steps.preview.outputs.url }}`
            })

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Deploy to staging
        run: npx zuplo deploy --api-key "$ZUPLO_API_KEY" --environment staging

  test-staging:
    name: Integration Tests (Staging)
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Run integration tests against staging
        run: npm run test:integration
        env:
          API_BASE_URL: ${{ vars.STAGING_API_URL }}

  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: test-staging
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Deploy to production
        run:
          npx zuplo deploy --api-key "$ZUPLO_API_KEY" --environment production
```

### What This Workflow Does

On **pull requests**, the pipeline runs two jobs in sequence:

1. **validate** -- Lints the OpenAPI specification and runs gateway
   configuration tests to catch errors before deployment.
2. **deploy-preview** -- Deploys to an isolated preview environment and comments
   the preview URL on the PR so reviewers can test against a live gateway.

On **merge to main**, the pipeline runs three jobs sequentially:

1. **deploy-staging** -- Deploys the merged configuration to the staging
   environment.
2. **test-staging** -- Runs integration tests against the staging deployment.
3. **deploy-production** -- Deploys to production, gated by a GitHub environment
   protection rule that can require manual approval.

The `environment: production` declaration on the final job enables GitHub's
[environment protection rules](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment),
so you can require approvals, restrict which branches can deploy, and set
deployment wait timers.

## GitLab CI Template

Here is the equivalent pipeline for GitLab CI. Create `.gitlab-ci.yml` in your
repository root:

```yaml
stages:
  - validate
  - deploy-preview
  - deploy-staging
  - test-staging
  - deploy-production

variables:
  NODE_VERSION: "20"

.node-setup: &node-setup
  image: node:${NODE_VERSION}
  before_script:
    - npm ci

validate:
  <<: *node-setup
  stage: validate
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - npx @redocly/cli lint openapi.json
    - npx zuplo dev &
    - sleep 10
    - npx zuplo test --endpoint http://localhost:9000

deploy-preview:
  <<: *node-setup
  stage: deploy-preview
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - npx zuplo deploy --api-key "$ZUPLO_API_KEY"
  environment:
    name: preview/$CI_MERGE_REQUEST_IID
    url: $PREVIEW_URL
    on_stop: stop-preview

stop-preview:
  stage: deploy-preview
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
  script:
    - echo "Preview environment cleaned up"
  environment:
    name: preview/$CI_MERGE_REQUEST_IID
    action: stop

deploy-staging:
  <<: *node-setup
  stage: deploy-staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - npx zuplo deploy --api-key "$ZUPLO_API_KEY" --environment staging
  environment:
    name: staging
    url: $STAGING_API_URL

test-staging:
  <<: *node-setup
  stage: test-staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - npm run test:integration
  variables:
    API_BASE_URL: $STAGING_API_URL

deploy-production:
  <<: *node-setup
  stage: deploy-production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
  script:
    - npx zuplo deploy --api-key "$ZUPLO_API_KEY" --environment production
  environment:
    name: production
    url: $PRODUCTION_API_URL
```

The GitLab template mirrors the GitHub Actions workflow. The production
deployment is set to `when: manual`, requiring an explicit click in the GitLab
UI to promote from staging to production. The preview environment uses dynamic
environment names tied to the merge request, so each MR gets its own isolated
deployment.

## Multi-Environment Deployment

Managing multiple environments requires a clear strategy for handling
configuration that varies between them. The gateway logic (routes, policies,
handlers) stays the same, but connection strings, upstream URLs, API keys, and
feature flags differ.

### Environment Variables and Secrets

Store environment-specific values as CI/CD secrets and variables, never in your
repository:

```bash
# GitHub Actions - set via repository settings or gh CLI
gh secret set ZUPLO_API_KEY --body "zpka_your_api_key"
gh variable set STAGING_API_URL --body "https://staging-api.example.com"
gh variable set PRODUCTION_API_URL --body "https://api.example.com"
```

In your gateway configuration (`config/routes.oas.json`), reference these
through environment variables rather than hardcoding values:

```json
{
  "paths": {
    "/v1/users": {
      "get": {
        "x-zuplo-route": {
          "handler": {
            "module": "$import(@zuplo/runtime)",
            "export": "urlRewriteHandler",
            "options": {
              "rewritePattern": "${env.UPSTREAM_URL}/users"
            }
          },
          "policies": {
            "inbound": []
          }
        }
      }
    }
  }
}
```

### Environment-Specific Configuration

For more involved configuration differences, use Zuplo's environment system to
manage settings per environment:

```typescript
// modules/config.ts
import { environment } from "@zuplo/runtime";

export function getUpstreamUrl(): string {
  return environment.UPSTREAM_URL ?? "http://localhost:3000";
}

export function getRateLimitConfig() {
  const isProd = environment.ZUPLO_ENVIRONMENT_STAGE === "production";
  return {
    requestsPerMinute: isProd ? 100 : 1000,
    windowMs: 60_000,
  };
}
```

This pattern keeps your gateway logic identical across environments while
allowing the operational parameters to vary. Development environments get
generous rate limits for testing, while production enforces tighter controls.

## Branch Preview Environments

Preview environments are one of the most powerful capabilities of a git-native
API gateway. Every pull request gets its own live, isolated gateway deployment
that reviewers and automated tests can interact with.

### How Zuplo Preview Environments Work

When you open a pull request, Zuplo automatically deploys a preview environment
with its own unique URL. This preview has the full gateway configuration from
your branch, running against your configured upstream services.

This means reviewers can:

- Send real HTTP requests to the preview gateway to verify new routes
- Test authentication and authorization policies against a live endpoint
- Verify that rate limiting behaves as expected
- Confirm that request/response transformations produce the correct output

### Why This Matters for API Testing

API changes are notoriously hard to review by reading configuration files alone.
A route definition in JSON or YAML looks correct until you send a request and
discover that a path parameter is not being passed through, or that a
transformation drops a required header.

Preview environments turn API gateway reviews from "does this config look right"
into "does this actually work." Your team can `curl` the preview URL, run
automated test suites against it, or point a frontend development environment at
it to test the full integration.

```bash
# Test the preview environment directly
curl -H "Authorization: Bearer $TEST_TOKEN" \
  https://your-preview-abc123.zuplo.app/v1/users

# Run your API test suite against the preview
API_BASE_URL=https://your-preview-abc123.zuplo.app \
  npm run test:integration
```

This feedback loop catches issues that static analysis and configuration
validation cannot detect. It moves the discovery of integration problems from
staging (or worse, production) to the pull request stage.

## Multi-Region Deployment

API gateways sit on the critical path of every request. Latency matters, and
deploying to a single region means users on the other side of the world pay a
round-trip penalty on every API call.

### The Traditional Approach

With traditional API gateways, multi-region deployment is an infrastructure
project. You provision gateway instances in each region, configure load
balancers, manage health checks, handle configuration synchronization, and deal
with the operational complexity of maintaining multiple deployments. Your CI/CD
pipeline grows proportionally:

```yaml
# The painful way -- deploying to each region individually
deploy-us-east:
  script: deploy --region us-east-1
deploy-eu-west:
  script: deploy --region eu-west-1
deploy-ap-southeast:
  script: deploy --region ap-southeast-1
# ... repeat for every region
```

### Zuplo's Edge Deployment Model

Zuplo takes a fundamentally different approach. When you run `npx zuplo deploy`,
your gateway configuration deploys to over 300 edge locations worldwide
automatically. There is no region selection, no multi-region pipeline
configuration, and no infrastructure to manage.

```bash
# One command. 300+ locations. Every deployment.
npx zuplo deploy --environment production
```

Every deployment is global by default. A user in Tokyo, a user in London, and a
user in Sao Paulo all hit the nearest edge location. Your CI/CD pipeline stays
simple because multi-region is not a deployment concern -- it is built into the
platform.

This architectural choice also simplifies your rollback story. A single
`git revert` and redeploy updates every edge location simultaneously rather than
requiring coordinated rollbacks across individual regional deployments.

## Rollback Strategies

Even with thorough testing and preview environments, issues will occasionally
reach production. Your rollback strategy determines whether this means minutes
of downtime or hours of scrambling.

### Git Revert and Redeploy

The simplest and most reliable rollback strategy for a git-native gateway is to
revert the problematic commit and let the pipeline redeploy:

```bash
# Identify the problematic commit
git log --oneline -10

# Revert it
git revert abc1234

# Push to trigger the pipeline
git push origin main
```

The pipeline deploys the reverted configuration through the same stages as any
other change. This approach is fast, auditable, and uses the exact same
deployment path as forward changes.

### Blue-Green Deployments

For zero-downtime rollbacks, a blue-green pattern maintains two production
environments. Traffic is routed to the active environment while the inactive one
receives the new deployment. If the new deployment is healthy, traffic switches
over. If not, traffic stays on the previous version.

```yaml
deploy-production:
  steps:
    - name: Deploy to inactive environment
      run: |
        INACTIVE=$(get-inactive-environment)
        npx zuplo deploy --environment $INACTIVE

    - name: Health check
      run: |
        curl --fail https://$INACTIVE_URL/health

    - name: Switch traffic
      run: |
        switch-traffic --to $INACTIVE
```

### Canary Deployments

Canary deployments route a small percentage of traffic to the new version while
the majority continues hitting the previous version. If error rates or latency
increase, the canary is rolled back before it affects most users.

This is particularly valuable for API gateways because you can monitor:

- Error rates on the canary vs. the stable version
- P50/P95/P99 latency differences
- Upstream error rates that might indicate a misconfigured proxy rule

Start with a small traffic percentage (5-10%), monitor for a defined period, and
gradually increase if metrics look healthy.

## Testing in the Pipeline

Automated testing is what makes CI/CD pipelines trustworthy. Without tests, a
pipeline is just automated deployment -- it moves code faster, including broken
code. Here are the testing stages to integrate into your API gateway pipeline.

### OpenAPI Validation

Your OpenAPI specification is the contract your API exposes to consumers.
Validate it on every pull request to catch breaking changes early:

```bash
# Validate the OpenAPI spec is structurally correct
npx @redocly/cli lint openapi.json

# Check for breaking changes against the main branch
npx @redocly/cli diff openapi.json --base main
```

### Contract Testing

Contract tests verify that your gateway's actual behavior matches the OpenAPI
specification. They send requests to a live gateway (the preview environment)
and validate that responses conform to the documented schemas:

```typescript
// tests/contract.test.ts
import { test, expect } from "@playwright/test";

test("GET /v1/users returns valid response", async ({ request }) => {
  const response = await request.get(`${process.env.API_BASE_URL}/v1/users`, {
    headers: {
      Authorization: `Bearer ${process.env.TEST_TOKEN}`,
    },
  });

  expect(response.status()).toBe(200);

  const body = await response.json();
  expect(body).toHaveProperty("data");
  expect(Array.isArray(body.data)).toBe(true);

  // Validate each user object has required fields
  for (const user of body.data) {
    expect(user).toHaveProperty("id");
    expect(user).toHaveProperty("email");
  }
});
```

### Integration Tests Against Preview Environments

Run your full integration test suite against the preview environment in the PR
pipeline. This validates the complete request flow: authentication, rate
limiting, request transformation, upstream routing, and response handling.

```yaml
# Add to your GitHub Actions PR workflow
test-preview:
  name: Integration Tests (Preview)
  needs: deploy-preview
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: "20"

    - name: Install dependencies
      run: npm ci

    - name: Run integration tests
      run: npm run test:integration
      env:
        API_BASE_URL: ${{ needs.deploy-preview.outputs.url }}
```

### Policy Testing

Test your gateway policies in isolation to verify they behave correctly before
deployment. For example, verify that rate limiting rejects requests after the
threshold:

```typescript
// tests/rate-limit.test.ts
import { test, expect } from "@playwright/test";

test("rate limiting enforces request threshold", async ({ request }) => {
  const baseUrl = process.env.API_BASE_URL;

  // Send requests up to the limit
  for (let i = 0; i < 10; i++) {
    const response = await request.get(`${baseUrl}/v1/health`);
    expect(response.status()).toBe(200);
  }

  // The next request should be rate limited
  const limited = await request.get(`${baseUrl}/v1/health`);
  expect(limited.status()).toBe(429);
});
```

## Get Started with Zuplo

If you are still configuring your API gateway through a dashboard, you are
leaving reliability on the table. Every manual change is a risk, every
environment inconsistency is a future incident, and every undocumented
modification is a compliance gap.

Zuplo is built for the workflow described in this guide. Your API gateway
configuration lives in Git, deploys through your existing CI/CD pipelines,
creates preview environments for every pull request, and deploys to 300+ edge
locations on every push. There is no separate infrastructure to manage and no
dashboard configuration to drift.

[Sign up for Zuplo](https://portal.zuplo.com) and deploy your first git-native
API gateway in minutes. Your CI/CD pipeline will thank you.