REST API Connectors
The gateway supports REST API Connectors, enabling hybrid architectures where GraphQL fields can resolve data from both gRPC services and REST APIs. This is perfect for gradual migrations, integrating third-party APIs, or bridging legacy systems.
Quick Start
use grpc_graphql_gateway::{Gateway, RestConnector, RestEndpoint, HttpMethod};
use std::time::Duration;
let rest_connector = RestConnector::builder()
.base_url("https://api.example.com")
.timeout(Duration::from_secs(30))
.default_header("Accept", "application/json")
.add_endpoint(RestEndpoint::new("getUser", "/users/{id}")
.method(HttpMethod::GET)
.response_path("$.data")
.description("Fetch a user by ID"))
.add_endpoint(RestEndpoint::new("createUser", "/users")
.method(HttpMethod::POST)
.body_template(r#"{"name": "{name}", "email": "{email}"}"#))
.build()?;
let gateway = Gateway::builder()
.with_descriptor_set_bytes(DESCRIPTORS)
.add_rest_connector("users_api", rest_connector)
.add_grpc_client("UserService", grpc_client)
.build()?;
GraphQL Schema Integration
REST endpoints are automatically exposed as GraphQL fields. The gateway generates:
- Query fields for GET endpoints
- Mutation fields for POST/PUT/PATCH/DELETE endpoints
Field names use the endpoint name directly (e.g., getUser, createPost).
Example GraphQL Queries
# Query a REST endpoint (GET /users/{id})
query {
getUser(id: "123")
}
# Mutation to create via REST (POST /users)
mutation {
createUser(name: "Alice", email: "alice@example.com")
}
Example Response
{
"data": {
"getUser": {
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
}
}
REST responses are returned as the JSON scalar type, preserving the full structure from the API.
RestConnector
The RestConnector is the main entry point for REST API integration.
Builder Methods
| Method | Description |
|---|---|
base_url(url) | Required. Base URL for all endpoints |
timeout(duration) | Default timeout (default: 30s) |
default_header(key, value) | Add header to all requests |
retry(config) | Custom retry configuration |
no_retry() | Disable retries |
log_bodies(true) | Enable request/response body logging |
with_cache(size) | Enable LRU response cache for GET requests |
interceptor(interceptor) | Add request interceptor |
transformer(transformer) | Custom response transformer |
add_endpoint(endpoint) | Add a REST endpoint |
RestEndpoint
Define individual REST endpoints with flexible configuration.
use grpc_graphql_gateway::{RestEndpoint, HttpMethod};
let endpoint = RestEndpoint::new("getUser", "/users/{id}")
.method(HttpMethod::GET)
.header("X-Custom-Header", "value")
.query_param("include", "profile")
.response_path("$.data.user")
.timeout(Duration::from_secs(10))
.description("Fetch a user by ID")
.return_type("User");
Path Templates
Use {variable} placeholders in paths:
RestEndpoint::new("getOrder", "/users/{userId}/orders/{orderId}")
When called with { "userId": "123", "orderId": "456" }, resolves to:
/users/123/orders/456
Query Parameters
Add templated query parameters:
RestEndpoint::new("searchUsers", "/users")
.query_param("q", "{query}")
.query_param("limit", "{limit}")
Body Templates
For POST/PUT/PATCH, define request body templates:
RestEndpoint::new("createUser", "/users")
.method(HttpMethod::POST)
.body_template(r#"{
"name": "{name}",
"email": "{email}",
"role": "{role}"
}"#)
If no body template is provided, arguments are automatically serialized as JSON.
Response Extraction
Extract nested data from responses using JSONPath:
// API returns: { "status": "ok", "data": { "user": { "id": "123" } } }
RestEndpoint::new("getUser", "/users/{id}")
.response_path("$.data.user") // Returns just the user object
Supported JSONPath:
$.field- Access field$.field.nested- Nested access$.array[0]- Array index$.array[0].field- Combined
Typed Responses
By default, REST endpoints return a JSON scalar blob. To enable field selection in GraphQL queries (e.g. { getUser { name email } }), you can define a response schema:
use grpc_graphql_gateway::{RestResponseSchema, RestResponseField};
RestEndpoint::new("getUser", "/users/{id}")
.with_response_schema(RestResponseSchema::new("User")
.field(RestResponseField::int("id"))
.field(RestResponseField::string("name"))
.field(RestResponseField::string("email"))
// Define a nested object field
.field(RestResponseField::object("address", "Address"))
)
This registers a User type in the schema and allows clients to select only the fields they need.
Mutations vs Queries
Endpoints are automatically classified:
- Queries: GET requests
- Mutations: POST, PUT, PATCH, DELETE
Override explicitly:
// Force a POST to be a query (e.g., search endpoint)
RestEndpoint::new("searchUsers", "/users/search")
.method(HttpMethod::POST)
.as_query()
HTTP Methods
use grpc_graphql_gateway::HttpMethod;
HttpMethod::GET // Read operations
HttpMethod::POST // Create operations
HttpMethod::PUT // Full update
HttpMethod::PATCH // Partial update
HttpMethod::DELETE // Delete operations
Authentication
Bearer Token
use grpc_graphql_gateway::{RestConnector, BearerAuthInterceptor};
use std::sync::Arc;
let connector = RestConnector::builder()
.base_url("https://api.example.com")
.interceptor(Arc::new(BearerAuthInterceptor::new("your-token")))
.build()?;
The interceptor adds: Authorization: Bearer your-token
API Key
use grpc_graphql_gateway::{RestConnector, ApiKeyInterceptor};
use std::sync::Arc;
let connector = RestConnector::builder()
.base_url("https://api.example.com")
.interceptor(Arc::new(ApiKeyInterceptor::x_api_key("your-api-key")))
.build()?;
The interceptor adds: X-API-Key: your-api-key
Custom Interceptor
Implement the RequestInterceptor trait for custom auth:
use grpc_graphql_gateway::{RequestInterceptor, RestRequest, Result};
use async_trait::async_trait;
struct CustomAuthInterceptor {
// Your auth logic
}
#[async_trait]
impl RequestInterceptor for CustomAuthInterceptor {
async fn intercept(&self, request: &mut RestRequest) -> Result<()> {
// Add custom headers, modify URL, etc.
request.headers.insert(
"X-Custom-Auth".to_string(),
"custom-value".to_string()
);
Ok(())
}
}
Retry Configuration
Configure automatic retries with exponential backoff:
use grpc_graphql_gateway::{RestConnector, RetryConfig};
use std::time::Duration;
let connector = RestConnector::builder()
.base_url("https://api.example.com")
.retry(RetryConfig {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(10),
multiplier: 2.0,
retry_statuses: vec![429, 500, 502, 503, 504],
})
.build()?;
Preset Configurations
// Disable retries
RetryConfig::disabled()
// Aggressive retries for critical endpoints
RetryConfig::aggressive()
Response Caching
Enable LRU caching for GET requests:
let connector = RestConnector::builder()
.base_url("https://api.example.com")
.with_cache(1000) // Cache up to 1000 responses
.build()?;
// Clear cache manually
connector.clear_cache().await;
Cache keys are based on endpoint name + arguments.
Multiple Connectors
Register multiple REST connectors for different services:
let users_api = RestConnector::builder()
.base_url("https://users.example.com")
.add_endpoint(RestEndpoint::new("getUser", "/users/{id}"))
.build()?;
let products_api = RestConnector::builder()
.base_url("https://products.example.com")
.add_endpoint(RestEndpoint::new("getProduct", "/products/{id}"))
.build()?;
let orders_api = RestConnector::builder()
.base_url("https://orders.example.com")
.add_endpoint(RestEndpoint::new("getOrder", "/orders/{id}"))
.build()?;
let gateway = Gateway::builder()
.add_rest_connector("users", users_api)
.add_rest_connector("products", products_api)
.add_rest_connector("orders", orders_api)
.build()?;
Executing Endpoints
Execute endpoints programmatically:
use std::collections::HashMap;
use serde_json::json;
let mut args = HashMap::new();
args.insert("id".to_string(), json!("123"));
let result = connector.execute("getUser", args).await?;
Custom Response Transformer
Transform responses before returning to GraphQL:
use grpc_graphql_gateway::{ResponseTransformer, RestResponse, Result};
use async_trait::async_trait;
use serde_json::Value as JsonValue;
use std::sync::Arc;
struct SnakeToCamelTransformer;
#[async_trait]
impl ResponseTransformer for SnakeToCamelTransformer {
async fn transform(&self, endpoint: &str, response: RestResponse) -> Result<JsonValue> {
// Transform snake_case keys to camelCase
Ok(transform_keys(response.body))
}
}
let connector = RestConnector::builder()
.base_url("https://api.example.com")
.transformer(Arc::new(SnakeToCamelTransformer))
.build()?;
Use Cases
| Scenario | Description |
|---|---|
| Hybrid Architecture | Mix gRPC and REST backends in one GraphQL API |
| Gradual Migration | Migrate from REST to gRPC incrementally |
| Third-Party APIs | Integrate external REST APIs (Stripe, Twilio, etc.) |
| Legacy Systems | Bridge legacy REST services with modern infrastructure |
| Multi-Protocol | Support teams using different backend technologies |
Best Practices
-
Set Appropriate Timeouts: Use shorter timeouts for internal services, longer for external APIs.
-
Enable Retries for Idempotent Operations: GET, PUT, DELETE are typically safe to retry.
-
Use Response Extraction: Extract only needed data with
response_pathto reduce payload size. -
Cache Read-Heavy Endpoints: Enable caching for frequently-accessed, rarely-changing data.
-
Secure Credentials: Use environment variables for API keys and tokens, not hardcoded values.
-
Log Bodies in Development Only: Enable
log_bodiesonly in development to avoid leaking sensitive data.