A declarative API gateway that unifies GraphQL and REST https://github.com/Grapthway/Gateway-Engine
236
The Future of Microservices Orchestration
A declarative API gateway that unifies GraphQL and REST services with a powerful, protocol-agnostic pipeline engine.
π― Quick Startβ β’ β¨ What's Newβ β’ ποΈ Architectureβ β’ π§ Examplesβ β’ π Documentationβ β’ π€ Contributingβ
Grapthway v2.0 is a major evolution, transforming from a GraphQL-only gateway into a Unified Orchestration Platform for both GraphQL and REST services.
| π Unified Pipeline Engine | β‘ High-Performance REST Proxy |
|---|---|
| Same powerful pre/post pipeline steps for both GraphQL fields and REST endpoints | REST routes without pipelines handled by direct reverse proxy |
| Protocol-agnostic workflows with consistent syntax | Zero performance overhead for non-orchestrated traffic |
| Create complex orchestrations spanning multiple service types | Seamless integration with existing REST services |
| π§ REST Body Mapping | π’ Enterprise HA Resume |
|---|---|
New bodyMapping attribute for declarative JSON request construction | Redis-backed gateways restart and resume previous state |
| Map data from pipeline context or use static values | Automatic token recovery without re-running /start |
| Available in pre, post, and rollback pipeline steps | Enhanced high-availability deployment patterns |
| πͺ Transactional Rollbacks |
|---|
onError handlers can trigger compensating rollback steps |
| Build resilient, transactional workflows |
| Safely undo actions across multiple services |
The v2.0 architecture introduces a unified Pipeline Executor at its core. This central component is responsible for orchestrating workflows for both GraphQL and REST requests, ensuring consistent logic for tasks like authentication, data enrichment, and logging. For REST routes without a defined pipeline, the gateway bypasses the executor and uses a high-performance reverse proxy to minimize latency.
graph TD
A[Client Request] --> B{GraphQL or REST?};
B -- GraphQL --> C[GraphQL Handler];
B -- REST --> D[REST Proxy Handler];
C --> E{Pipeline Configured?};
D --> E;
E -- Yes --> F[Unified Pipeline Executor];
F -- Pre-Pipeline --> G[Proxy to Downstream Service];
G -- Response --> F;
F -- Post-Pipeline --> H[Compose Final Response];
E -- No (REST Only) --> I[High-Speed Reverse Proxy];
I --> G;
H --> Z[Client Response];
sequenceDiagram
participant Handler as Request Handler
participant Executor as Pipeline Executor
participant Service as Downstream Service
Handler->>Executor: ExecutePrePipeline(config, context)
Executor->>Service: Execute Step 1 (e.g., Auth)
Service-->>Executor: Step 1 Response
Executor->>Executor: Update Context
Executor->>Service: Execute Step 2 (e.g., Get Data)
Service-->>Executor: Step 2 Response
Executor-->>Handler: Return Pipeline Context
Handler->>Service: Main Request
Service-->>Handler: Main Response
Handler->>Executor: ExecutePostPipeline(config, mainResponse)
Executor->>Service: Enrichment Call
Service-->>Executor: Enrichment Data
Executor-->>Handler: Merged Final Data
Schema stitching allows you to extend types defined in one service with fields resolved by another, creating a rich, interconnected data graph. This feature is specific to GraphQL services.
graph TD
subgraph "Unified Schema (in Gateway)"
C["type User { id: ID, name: String, orderHistory: [Order] }"]
end
subgraph "user-service"
A["type User { id: ID, name: String }"]
end
subgraph "order-service"
B["type Query { getOrdersByUserId(userId: ID!): [Order] }"]
end
C -- Stitched from --> A;
C -- Extended with field resolved by --> B;
| π₯ Core Capabilities | π‘οΈ Production Features |
|---|---|
| Unified API Gateway for GraphQL and REST | Intelligent Blue-Green Deployments via health checks |
| Declarative, Protocol-Agnostic Pipelines | Redis Persistence for enterprise deployments |
| Dynamic GraphQL Schema Stitching | Real-time Admin Dashboard with live metrics |
| Transactional Workflows with Rollbacks | WebSocket Log Streaming for instant debugging |
| Automatic Service Discovery & Health Monitoring | Automatic Cleanup of stale services |
| Round-Robin Load Balancing | GraphQL Subscriptions over WebSockets |
| Context Propagation via X-Ctx-* headers | High-Performance REST Proxy for legacy systems |
Choose your edition and get started in seconds with the new v2.0 images:
# π Community Edition (Perfect for development)
docker run -d -p 5000:5000 --name my-community-gateway farisbahdlor/grapthway:community-v2.0
# π’ Enterprise Edition (Requires Redis for persistence)
docker run -d -p 5000:5000 \
-e STORAGE_TYPE=redis \
-e REDIS_ADDR=your-redis-host:6379 \
--name my-enterprise-gateway farisbahdlor/grapthway:enterprise-v2.0
On the first run, initialize the gateway to get your admin tokens:
curl http://localhost:5000/start
π‘ Pro Tip: Store your admin tokens securelyβyou'll need them for service registration and dashboard access. Enterprise users with Redis only need to do this once.
From your microservice, send a POST request to /admin/health every 30 seconds. This single payload can now define all your service's pipelines, for both GraphQL and REST.
// Example: A GraphQL-only authentication service
{
"service": "auth-service", // The unique name of your service.
"url": "http://auth-internal:4001/graphql", // The internal address of your service.
"type": "graphql", // The primary type of this service.
"subgraph": "auth", // The logical group this service's schema belongs to.
"schema": "type User { ... } type Mutation { login(user: String!): String }", // The GraphQL schema definition.
// Defines pipelines for specific GraphQL fields.
"middlewareMap": {
"login": { // This pipeline attaches to the 'login' mutation.
"pre": [
{
"service": "rate-limiter-rest", // Call a REST service before the main mutation.
"method": "POST",
"path": "/check",
"onError": { "stop": true } // If the rate-limiter fails, stop the entire request.
}
]
}
},
// Defines how to "stitch" fields from this service onto types from other services.
"stitchingConfig": {
"User": { // Extend the 'User' type.
"authInfo": { // With a new field called 'authInfo'.
"service": "auth-service", // This service will provide the data.
"resolverField": "getUserById", // By calling its 'getUserById' query.
"argsMapping": { "id": "id" } // Map the parent User's 'id' to the resolver's 'id' argument.
}
}
}
}
// Example: A REST-only legacy inventory service
{
"service": "inventory-api", // The unique name for the REST service.
"url": "http://inventory-legacy:3000", // The base URL for the service.
"type": "rest", // The service type is REST.
"path": "/v1/inventory", // The base path prefix for all routes from this service.
// Defines pipelines for specific REST routes.
"restPipelines": {
"POST /v1/inventory/update": { // Attaches to the 'POST' method on this specific route.
"pre": [
{
"service": "auth-service", // A GraphQL service used for auth.
"field": "verifyAdmin", // Call this GraphQL field.
"passHeaders": ["Authorization"], // Pass the auth header from the original request.
"onError": { "stop": true } // If auth fails, stop the request.
}
]
}
}
}
// Example: A hybrid service with both GraphQL and REST capabilities
{
"service": "products-service",
"url": "http://products-service-internal:8000",
"type": "graphql", // The primary type. REST routes are an addition.
"subgraph": "products",
"schema": "type Query { getProduct(id: ID!): Product } type Product { id: ID name: String }",
// Pipelines for GraphQL fields.
"middlewareMap": {
"getProduct": {
"pre": [{
"service": "auth-service",
"field": "validateSession",
"onError": { "stop": true }
}]
}
},
// Pipelines for REST endpoints also served by this service.
"restPipelines": {
"POST /products/{id}/inventory": {
"pre": [{
"service": "auth-service",
"field": "verifyAdmin",
"onError": { "stop": true }
}]
}
},
// Schema stitching configuration.
"stitchingConfig": {
"Order": { // Extends the 'Order' type (defined in another service).
"productDetails": { // Adds a 'productDetails' field.
"service": "products-service", // This service resolves the field.
"resolverField": "getProduct", // Using its 'getProduct' query.
"argsMapping": { "id": "productId" } // Maps the parent Order's 'productId' to the 'id' argument.
}
}
}
}
A user places an order via a GraphQL mutation. The pipeline validates the user (GraphQL), checks stock (REST), and after the main mutation, sends a confirmation email (REST).
// Attached to GraphQL mutation: placeOrder(items: [ItemInput!]): Order
// This mutation is assumed to return an object like: { "id": "ord_123", "buyerId": "usr_456", "total": 99.99 }
{
"placeOrder": {
"pre": [
{
// 1. First, call the 'auth-service-gql' GraphQL service to validate the user session.
"service": "auth-service-gql",
"field": "validateSession",
"passHeaders": ["Authorization"],
"selection": ["user { id email }"], // Only ask for the fields we need.
"assign": { "userContext": "user" }, // Assign the 'user' object from the response to the 'userContext' key in the pipeline context.
"onError": { "stop": true } // If this fails, stop the entire process.
},
{
// 2. Next, call the 'inventory-api-rest' REST service to check stock.
"service": "inventory-api-rest",
"method": "POST",
"path": "/v1/stock/check-availability",
"bodyMapping": {
// Get the 'items' from the original GraphQL mutation arguments and map them to the request body.
"items_to_check": "args.items"
},
"onError": { "stop": true }
}
],
"post": [
{
// 3. After the main 'placeOrder' mutation succeeds, call the email REST service.
"service": "email-api-rest",
"method": "POST",
"path": "/v2/send/order-confirmation",
"bodyMapping": {
// Get the user's email from the context we saved in the 'pre' step.
"recipient_email": "userContext.email",
// Get the 'id' field from the main 'placeOrder' mutation's response.
"order_id": "id",
// Get the 'total' field from the main 'placeOrder' mutation's response.
"order_total": "total"
},
"concurrent": true // Run this as fire-and-forget; don't wait for it to complete.
}
]
}
}
A user creates a post via a POST to a REST endpoint. The pipeline validates the session (GraphQL), runs content moderation (REST), and after success, notifies followers (GraphQL).
// Attached to REST endpoint: POST /v3/posts
// The main REST handler is assumed to return a response like: { "id": "post_789", "content": "..." }
{
"POST /v3/posts": {
"pre": [
{
// 1. Authenticate the user via a GraphQL call.
"service": "user-session-gql",
"field": "getActiveUser",
"passHeaders": ["Authorization"],
"selection": ["id"],
"assign": { "user": "" }, // Assign the entire response object (e.g., { "id": "..." }) to the 'user' key.
"onError": { "stop": true }
},
{
// 2. Run the post content through a moderation REST API.
"service": "moderation-api-rest",
"method": "POST",
"path": "/filter/text",
"bodyMapping": {
// Get the 'content' from the body of the original REST request.
"text_content": "request.body.content"
},
"onError": { "stop": true }
}
],
"post": [
{
// 3. After the post is created, trigger a GraphQL mutation to notify followers.
"service": "notification-gql",
"operation": "mutation",
"field": "notifyFollowersOfNewPost",
"argsMapping": {
// Map the 'id' from the main REST response to the 'postId' argument.
"postId": "id",
// Map the user's ID from the context (set in the 'pre' step) to the 'authorId' argument.
"authorId": "user.id"
},
"concurrent": true
}
]
}
}
A pre pipeline step authorizes a payment (REST). A subsequent step to reserve tickets (GraphQL) fails, triggering a rollback that calls the payment provider's void endpoint to cancel the authorization.
// Attached to GraphQL mutation: reserveTickets(showId: ID!, quantity: Int!)
{
"reserveTickets": {
"pre": [
{
// 1. Authorize payment with a third-party REST API.
"service": "payment-provider-api",
"method": "POST",
"path": "/v1/authorize_payment",
"bodyMapping": {
// Use data from the GraphQL arguments.
"amount": "args.ticketPrice",
"currency": "USD", // Use a static value.
"card_token": "args.paymentToken"
},
"assign": { "paymentAuth": "" }, // Store the entire auth response (e.g., { "transactionId": "..." }) in the context.
"onError": { "stop": true }
},
{
// 2. Attempt to reserve tickets. Let's assume this step fails.
"service": "ticketing-service-gql",
"field": "createReservation",
"argsMapping": { "showId": "args.showId" },
"assign": { "reservation": ""},
// Because this blocking step failed, the 'rollback' action is triggered.
"onError": {
"stop": true,
"rollback": [
{
// This is the compensating action: void the payment authorization.
"service": "payment-provider-api",
"method": "POST",
"path": "/v1/void_authorization",
"bodyMapping": {
// Get the transaction ID from the context we saved in the first step.
"authorization_id": "paymentAuth.transactionId",
// Provide a static reason code for the void action.
"reason_code": "INVENTORY_UNAVAILABLE"
}
}
]
}
}
]
}
}
An IoT device sends data to a REST endpoint. A post pipeline checks for anomalies (GraphQL) and, if an alert is found, sends a notification to a Slack webhook (REST).
// Attached to REST endpoint: POST /ingest/telemetry
{
"POST /ingest/telemetry": {
"pre": [{
// 1. Validate the device's token before processing the telemetry data.
"service": "device-auth-api",
"method": "POST",
"path": "/token/introspect",
"passHeaders": ["X-Device-Token"],
"assign": { "device": "" }, // Store device info in the context.
"onError": { "stop": true }
}],
"post": [
{
// 2. After data ingestion, call a GraphQL service to check for anomalies. This call is blocking.
"service": "anomaly-detection-gql",
"field": "analyzeTelemetry",
"argsMapping": { "payload": "payload" }, // Assumes main handler returns { "payload": ... }
"selection": ["alert { level message }"],
"assign": { "anomalyResult": "" }, // Store the analysis result in the context.
"concurrent": false // 'false' is default, but explicit here. The next step depends on this result.
},
{
// 3. This step runs after the anomaly check. It will send a Slack message.
// Note: The pipeline doesn't have conditional logic. If no alert is found, this may send an empty message or fail gracefully.
"service": "slack-webhook-rest",
"method": "POST",
"path": "/T012345/B67890/xyz...", // The unique part of the Slack webhook URL.
"bodyMapping": {
"text": "anomalyResult.alert.message" // Get the alert message from the previous step's result.
},
"concurrent": true
}
]
}
}
| Endpoint | Purpose |
|---|---|
/admin/services | List all registered services and instances |
/admin/schema | View raw SDL schemas |
/admin/gateway-status | Detailed gateway metrics |
/admin/pipelines | Active pipeline configurations |
| Feature | π Community | π’ Enterprise |
|---|---|---|
| Storage | In-memory | Redis persistence |
| Services per subgraph | Up to 3 | Unlimited |
| Max subgraphs | 2 | Unlimited |
| REST endpoints | β Full support | β Full support |
| Pipeline engine | β Full support | β Full support |
| Log retention | 2 days | Configurable |
| HA Resume | β | β |
| Support | Community | Priority + SLA |
sequenceDiagram
participant C as Client
participant G as Gateway
participant M as Pipeline Engine
participant GS as GraphQL Services
participant RS as REST Services
participant D as Dashboard
C->>G: GraphQL/REST Request
G->>M: Execute Pre-Pipeline
M->>GS: Route to GraphQL Service(s)
M->>RS: Route to REST Service(s)
GS-->>M: GraphQL Response
RS-->>M: REST Response
M->>G: Execute Post-Pipeline
G-->>C: Final Response
G->>D: Stream Logs
| Issue | Solution |
|---|---|
| π΄ Service not appearing | Check announcement frequency (e.g., every 30s) and network connectivity. |
| π‘ Schema not updating | Validate schema format and check gateway logs for parsing or merging errors. |
| π Pre-pipeline not running | Ensure the service is announcing its middlewareMap or restPipelines correctly. |
| π΅ Dashboard not loading | Confirm admin token and WebSocket connectivity to the gateway. |
| π£ REST endpoint not found | Check the path configuration and ensure the service type is set correctly. |
# Check gateway status
curl "http://localhost:5000/admin/gateway-status?token=${ADMIN_TOKEN}"
# View active services
curl "http://localhost:5000/admin/services?token=${ADMIN_TOKEN}"
# Inspect pipeline configurations
curl "http://localhost:5000/admin/pipelines?token=${ADMIN_TOKEN}"
# Test REST endpoint directly
curl "http://localhost:5000/your-rest-path" -X POST
For comprehensive documentation, advanced configurations, and best practices:
π Complete Engineering Manualβ
We welcome contributions from the community! Whether it's bug reports, or feature suggestions, please feel free to:
Grapthway is available in two editions:
Your use of any Grapthway Docker image is subject to the terms and conditions outlined in our Software License Agreement.
Please read the full agreement here: LICENSE.mdβ
Built with β€οΈ by the Grapthway team. Special thanks to:
Ready to unify your microservices architecture?
π Get Started with v2.0β β’ π¬ Join Communityβ β’ π¦ Follow Updatesβ
Content type
Image
Digest
sha256:f14328799β¦
Size
8 MB
Last updated
about 1 year ago
docker pull farisbahdlor/grapthway:community-v2.0