If you have read about Zuplo and want to build and deploy something real, this actionable guide is for you. Zuplo is fast to pick up, but its documentation is scattered across portal flows, CLI commands, and articles. Here’s a step-by-step tutorial to create a project, expose a route, add API key authentication and rate limiting, write a custom TypeScript policy, deploy to the edge, and test the API with Apidog.
By the end, you’ll have a fully working API gateway in front of your origin API, with authentication, rate limiting, an auto-generated developer portal, and Git-based CI workflows. The process takes about thirty minutes.
If you’re still evaluating Zuplo, check out our companion post: What is the Zuplo API gateway. For deeper details, see the Zuplo documentation.
TL;DR
- Sign up at portal.zuplo.com or scaffold a project locally with
npm create zuplo. - Define routes in
config/routes.oas.jsonand forward to your API using the URL Forward Handler. - Add inbound policies (API key auth, rate limit, schema validation) via the route file or Route Designer.
- Write custom logic as TypeScript modules in
modules/; access request, context, and environment in code. - Push to a Git branch to deploy a preview; merge to deploy to production (300+ edge locations).
- Test all routes with Apidog before production.
- Free tier: 100K requests/month. Builder plan: $25/month.
Prerequisites
You’ll need:
- A Zuplo account.
- An origin API. If you don’t have one, use
https://echo.zuplo.io(an echo server). - Node.js 18+ (for CLI usage).
For local development, use a code editor. VS Code with the TypeScript extension is recommended. Pair it with the Apidog VS Code extension for fast in-editor requests.
Step 1: Create your Zuplo project
Start either via the web portal or CLI.
Option A: Portal-first
- Sign in at portal.zuplo.com.
- Click “New Project” and name it (e.g.,
acme-gateway). - Choose “Empty Project”.
- The code tab opens with the starter file tree.
By default, the portal links the project to a managed Git repo. You can connect your own GitHub, GitLab, Bitbucket, or Azure DevOps repo from Settings.
Option B: CLI-first
The CLI scaffolds a local project for IDE-based development and native Git.
npm create zuplo@latest -- --name acme-gateway
cd acme-gateway
npm install
npm run dev
The dev server runs on port 9000, with the Route Designer UI at http://localhost:9100. Changes in code or designer hot-reload immediately.
To link your local project to your Zuplo account for deployment:
npx zuplo link
Pick your account and environment. Deploy with:
npx zuplo deploy
Step 2: Define your first route
Edit config/routes.oas.json (OpenAPI 3 with Zuplo extensions). Example: forward GET /v1/products to your origin API.
{
"openapi": "3.1.0",
"info": { "title": "Acme Gateway", "version": "1.0.0" },
"paths": {
"/v1/products": {
"get": {
"summary": "List products",
"operationId": "list-products",
"x-zuplo-route": {
"corsPolicy": "anything-goes",
"handler": {
"export": "urlForwardHandler",
"module": "$import(@zuplo/runtime)",
"options": {
"baseUrl": "${env.ORIGIN_URL}"
}
},
"policies": { "inbound": [] }
},
"responses": {
"200": { "description": "Success" }
}
}
}
}
}
- The
x-zuplo-routeextension defines Zuplo settings inside OpenAPI. -
handlerusesurlForwardHandlerto proxy to your backend. -
${env.ORIGIN_URL}is an environment variable for the backend URL; set it in the portal (Settings > Environment Variables) or inconfig/.env.
Test locally at http://localhost:9000/v1/products. If using the echo server, you’ll get your request echoed back.
Step 3: Add API key authentication
To secure your API, use Zuplo’s managed API key service.
Update the route’s policies:
"policies": {
"inbound": ["api-key-auth"]
}
Add the policy to config/policies.json:
{
"name": "api-key-auth",
"policyType": "api-key-inbound",
"handler": {
"export": "ApiKeyInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"allowUnauthenticatedRequests": false
}
}
}
Create an API consumer:
- Go to Services > API Key Service in the portal.
- Click “Create Consumer”.
- Set the subject (e.g.,
acme-customer-1). - Add an admin email.
- Copy the generated API key.
Test authentication:
Without a key (should return 401):
curl -i https://YOUR-PROJECT.zuplo.app/v1/products
With an API key (should return 200):
curl -i https://YOUR-PROJECT.zuplo.app/v1/products \
-H "Authorization: Bearer YOUR_API_KEY"
To test in a client, import your OpenAPI spec into Apidog, set a global Authorization header, and bind api_key as an environment variable for fast requests.
Step 4: Rate limit the route
Protect your API with rate limiting (per IP, key, or custom attribute).
Update policies:
"policies": {
"inbound": ["api-key-auth", "rate-limit-by-key"]
}
Define the rate-limit policy in config/policies.json:
{
"name": "rate-limit-by-key",
"policyType": "rate-limit-inbound",
"handler": {
"export": "RateLimitInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"rateLimitBy": "sub",
"requestsAllowed": 60,
"timeWindowMinutes": 1
}
}
}
-
rateLimitBy: "sub"keys the limit to the authenticated subject (API key). - The 61st request in one minute returns 429.
Test:
for i in {1..70}; do
curl -s -o /dev/null -w "%{http_code}\n" \
https://YOUR-PROJECT.zuplo.app/v1/products \
-H "Authorization: Bearer YOUR_API_KEY"
done | sort | uniq -c
Expect 60 responses with 200 and 10 with 429.
Step 5: Validate request payloads
For POST routes, enforce schema validation at the gateway using the OpenAPI schema.
Sample POST route in routes.oas.json:
"/v1/products": {
"post": {
"summary": "Create product",
"operationId": "create-product",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name", "priceCents"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"priceCents": { "type": "integer", "minimum": 1 },
"category": { "type": "string", "enum": ["food", "drink"] }
}
}
}
}
},
"x-zuplo-route": {
"handler": { /* same as above */ },
"policies": {
"inbound": [
"api-key-auth",
"rate-limit-by-key",
"validate-request"
]
}
}
}
}
Define the validation policy:
{
"name": "validate-request",
"policyType": "open-api-request-validation-inbound",
"handler": {
"export": "OpenApiRequestValidationInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"validateBody": "reject"
}
}
}
With this, malformed requests are rejected with 400 before reaching your origin. In Apidog, create test cases for valid, invalid, and edge-case requests.
Step 6: Write a custom TypeScript policy
For custom logic, write a policy module. Example: set Cache-Control headers based on user plan.
Create modules/tiered-cache.ts:
import { ZuploRequest, ZuploContext } from "@zuplo/runtime";
interface PolicyOptions {
paidPlanHeader: string;
paidMaxAge: number;
}
export default async function (
response: Response,
request: ZuploRequest,
context: ZuploContext,
options: PolicyOptions,
): Promise<Response> {
const plan = request.user?.data?.plan ?? "free";
if (plan === "free") {
response.headers.set("Cache-Control", "no-store");
} else {
response.headers.set(
"Cache-Control",
`public, max-age=${options.paidMaxAge}`,
);
}
context.log.info(`Cache header set for plan=${plan}`);
return response;
}
Add to config/policies.json:
{
"name": "tiered-cache",
"policyType": "custom-code-outbound",
"handler": {
"export": "default",
"module": "$import(./modules/tiered-cache)",
"options": {
"paidPlanHeader": "x-plan",
"paidMaxAge": 300
}
}
}
Reference in the route:
"policies": {
"inbound": ["api-key-auth", "rate-limit-by-key"],
"outbound": ["tiered-cache"]
}
You can unit-test this function with Vitest or Jest by providing mock requests and responses.
Step 7: Deploy to the edge
Deployment is handled via Git.
git add .
git commit -m "Add products gateway with auth, rate limit, and tiered cache"
git push origin feature/products-gateway
Zuplo builds a preview environment for each branch, available at a unique subdomain (e.g., https://acme-gateway-feature-products-gateway-abc123.zuplo.app). All policies are active; the environment’s ORIGIN_URL is used.
Test your preview with Apidog by setting the preview URL as a new environment. Run your test suite. When ready, deploy to production:
git checkout main
git merge feature/products-gateway
git push origin main
Production deploys go live globally within about sixty seconds. Promote and rollback are just Git operations.
Step 8: Generate the developer portal
Your developer portal is at https://YOUR-PROJECT.developers.zuplo.com and updates on every deploy. Features:
- Auto-generated pages per route, with schema and try-it console.
- Code samples in cURL, JavaScript, Python, Go, etc.
- Self-serve API key registration.
- Branding controls in Developer Portal > Settings.
If your OpenAPI spec is well-documented, the portal is ready out-of-the-box. For customization, fork the Zuplo developer portal repo (Next.js app). Most teams use the hosted version.
Step 9: Test everything with Apidog
Test every route, policy, and error state to prevent production issues. Apidog streamlines this.
Recommended workflow:
- Import your OpenAPI spec from
https://YOUR-PROJECT.zuplo.app/openapi. Apidog creates requests for each operation. - Create environments for
local,preview,productionwith correspondingbase_urlandapi_key. - Save at least three requests per route: happy path, auth failure, rate-limit trigger.
- Use Apidog’s test scenarios to chain requests and assert on responses.
- Generate code samples in your team’s language and add to runbooks.
If transitioning from Postman, see the API testing without Postman guide. Download Apidog if you haven’t yet.
Common questions about using Zuplo
How do I switch a route between environments without changing the spec?
Use environment variables. Define ORIGIN_URL per environment (portal Settings or config/.env), and reference ${env.ORIGIN_URL} in your route handler. The route config stays unchanged.
Can I run Zuplo offline?
Yes. npm run dev runs a local gateway at port 9000 with Route Designer at 9100. All features work locally except the managed API key service (requires linking with npx zuplo link for cloud key management).
How do I roll back a bad deploy?
Run git revert on the merge commit and push. Zuplo redeploys the previous state. Git history is the source of truth—no separate rollback UI.
What happens to in-flight requests during a deploy?
Deploys are atomic. In-flight requests finish on the old version; new requests go to the new version. No downtime.
Can I use Zuplo with gRPC or WebSockets?
Yes. urlForwardHandler proxies WebSocket upgrades; gRPC is supported via the gRPC handler. REST and GraphQL are first-class.
How do I expose my Zuplo API to AI agents?
Add the MCP Server Handler to a route, point it at your OpenAPI spec, and select operations to expose. Auth and rate-limit policies apply. See the Zuplo MCP Server documentation.
How much does the gateway cost in production?
Free tier: 100K requests/month. Builder: 1M requests for $25/month; additional requests: $100 per 100K. Enterprise starts at $1,000/month. See Zuplo pricing.
Conclusion
You now have a Zuplo gateway with API key auth, per-key rate limiting, request validation, a custom TypeScript outbound policy, and a developer portal—all deployed via Git to the edge. This setup supports previews, production, and AI agent access through MCP.
The key to stability is a strong test loop. Use Apidog against every preview before merging, and you’ll catch broken auth, missing schema fields, and overly generous rate limits before they reach production. Download Apidog and integrate it into your workflow.


Top comments (0)