API Gateway
This guide covers how to build a production-ready API Gateway for the Eloquent platform. The API Gateway serves as the single HTTP entry point for all frontend-to-backend communication.
What is an API Gateway?
An API Gateway is a thin HTTP reverse proxy that serves as the single entry point for all frontend requests. It handles:
- Authentication: JWT validation before requests reach backend
- Authorization: Scope-based access control per route
- Routing: Maps HTTP paths to backend services
- Cross-cutting concerns: CORS, rate limiting, logging, caching
HTTP Proxy Pattern (Required)
IMPORTANT: The API Gateway MUST use the HTTP Proxy pattern. Each backend service exposes REST endpoints, and the gateway proxies requests directly to them.
Browser → API Gateway → HTTP → Backend Service (REST API)
Architecture
Frontend (Next.js App)
│
HTTP REST API
│
▼
┌─────────────────────┐
│ API Gateway │
│ │
│ ┌───────────────┐ │
│ │ Middleware │ │
│ │ 1. Recovery │ │
│ │ 2. RequestID │ │
│ │ 3. CORS │ │
│ │ 4. Logging │ │
│ │ 5. JWT Auth │ │
│ │ 6. Scopes │ │
│ └───────────────┘ │
│ │ │
│ HTTP Reverse Proxy│
└─────────────────────┘
│
HTTP Internal
│
▼
┌─────────────────────┐
│ Backend Services │
│ (Go Microservices) │
└─────────────────────┘
Route Configuration
Routes are defined in routes.yaml:
version: "1.0"
public_paths:
- /health
- /ready
- /api/v1/docs
routes:
# READ operation (GET) - no scope required, just auth
- path: /api/v1/agents
method: GET
proxy_upstream: $AGENTS_SERVICE_URL
auth_required: true
cache_ttl: 60s
# WRITE operation (POST) - requires scope
- path: /api/v1/agents
method: POST
proxy_upstream: $AGENTS_SERVICE_URL
auth_required: true
scopes:
- write:agents
# WRITE operation (PUT/DELETE) - requires scope
- path: /api/v1/agents/{id}
method: PUT
proxy_upstream: $AGENTS_SERVICE_URL
auth_required: true
scopes:
- write:agents
Route Configuration Options
| Option | Description | Example |
|---|---|---|
path | URL path (supports {param} placeholders) | /api/v1/widgets/{id} |
method | HTTP method | GET, POST, PUT, DELETE |
proxy_upstream | Backend service URL (env var) | $WIDGET_SERVICE_URL |
auth_required | Require JWT authentication | true / false |
scopes | Required JWT scopes | ["write:widgets"] |
cache_ttl | Response cache duration | 60s, 5m |
timeout | Request timeout | 300s |
description | Documentation | "Create widget" |
JWT Authentication
Token Structure
{
org_id: "org-uuid",
user_id: "user-uuid",
email: "user@example.com",
role: "user",
scopes: ["read", "write", "write:agents"],
product: "eloquent"
}
Frontend Token Generation
The frontend generates JWT tokens via a Next.js API route:
// app/api/gateway-token/route.ts
import * as jose from "jose";
export async function GET() {
const session = await auth();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const scopes = [
"read",
"write",
"write:agents",
"write:workflows",
"write:entities",
];
const token = await new jose.SignJWT({
org_id: orgId,
user_id: user.id,
email: user.email,
role: "user",
scopes,
product: "eloquent",
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setIssuer("eloquent-gateway")
.setAudience("eloquent-api")
.setExpirationTime("1h")
.sign(secret);
return NextResponse.json({ token, expiresIn: 3600 });
}
Header Forwarding
The gateway extracts JWT claims and forwards them as headers to backend services:
| JWT Claim | Forwarded Header |
|---|---|
org_id | X-Org-ID |
user_id | X-User-ID |
email | X-User-Email |
role | X-User-Role |
Backend services use these headers for authorization:
func (h *Handler) CreateWidget(w http.ResponseWriter, r *http.Request) {
orgID := r.Header.Get("X-Org-ID")
userID := r.Header.Get("X-User-ID")
if orgID == "" {
writeError(w, http.StatusBadRequest, "MISSING_ORG_ID", "X-Org-ID required")
return
}
// ... handle request
}
Error Responses
The gateway returns standardized error responses:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token",
"request_id": "req-abc123"
},
"metadata": {
"success": false,
"timestamp": "2024-03-01T10:00:00Z"
}
}
Common Error Codes
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid token |
| 401 | EXPIRED_TOKEN | Token has expired |
| 403 | FORBIDDEN | Access denied |
| 403 | INSUFFICIENT_SCOPE | Token lacks required scope |
| 429 | RATE_LIMITED | Too many requests |
| 503 | SERVICE_UNAVAILABLE | Backend service down |
Adding a New Route
- Add route to
routes.yaml:
- path: /api/v1/widgets
method: POST
proxy_upstream: $WIDGET_SERVICE_URL
auth_required: true
scopes:
- write:widgets
- Add service URL to environment:
WIDGET_SERVICE_URL=http://widget-service:80
- Add scope to frontend token (if new):
const scopes = [
// ... existing scopes
"write:widgets",
];
- Rebuild gateway:
make docker-rebuild-api-gateway
Next Steps
- Backend Services - Creating backend endpoints
- Frontend Apps - Integrating with the gateway
- Error Handling - Unified error patterns