Documentation Index
Fetch the complete documentation index at: https://mintlify.com/microservices-patterns/ftgo-application/llms.txt
Use this file to discover all available pages before exploring further.
This page explains how the FTGO application uses the API Gateway pattern through two separate gateway implementations: a REST-based gateway built with Spring Cloud Gateway and Spring WebFlux (ftgo-api-gateway), and a GraphQL gateway built with Node.js (ftgo-api-gateway-graphql). Both act as the single entry point for external clients, hiding the internal service topology.
Why an API Gateway
Without a gateway, a mobile or web client must know the address of every downstream service, make multiple round trips to assemble a single screen’s worth of data, and handle partial failures from each service independently. An API gateway solves this by:
- Routing simple requests directly to the owning service.
- Composing responses that require data from several services into one reply.
- Abstracting the internal service topology from external clients.
REST Gateway — ftgo-api-gateway
The REST gateway runs on port 8087 and is implemented as a Spring Boot application using Spring Cloud Gateway for routing and Spring WebFlux for reactive API composition.
Routing Configuration
OrderConfiguration registers Spring Cloud Gateway routes for write operations and the order-history list query, plus a WebFlux RouterFunction for the composed order-detail endpoint:
// OrderConfiguration.java
@Configuration
@EnableConfigurationProperties(OrderDestinations.class)
public class OrderConfiguration {
@Bean
public RouteLocator orderProxyRouting(RouteLocatorBuilder builder,
OrderDestinations orderDestinations) {
return builder.routes()
// POST/PUT /orders → Order Service
.route(r -> r.path("/orders").and().method("POST")
.uri(orderDestinations.getOrderServiceUrl()))
.route(r -> r.path("/orders").and().method("PUT")
.uri(orderDestinations.getOrderServiceUrl()))
.route(r -> r.path("/orders/**").and().method("POST")
.uri(orderDestinations.getOrderServiceUrl()))
.route(r -> r.path("/orders/**").and().method("PUT")
.uri(orderDestinations.getOrderServiceUrl()))
// GET /orders → Order History Service (CQRS read model)
.route(r -> r.path("/orders").and().method("GET")
.uri(orderDestinations.getOrderHistoryServiceUrl()))
.build();
}
@Bean
public RouterFunction<ServerResponse> orderHandlerRouting(
OrderHandlers orderHandlers) {
// GET /orders/{orderId} → composed response
return RouterFunctions.route(
GET("/orders/{orderId}"),
orderHandlers::getOrderDetails);
}
}
Write requests (POST, PUT) are forwarded transparently to the Order Service. The list query (GET /orders) is forwarded to the Order History Service. Only the single-order detail endpoint (GET /orders/{orderId}) performs API composition.
API Composition for Order Details
OrderHandlers.getOrderDetails calls four downstream services in parallel using Reactor’s Mono.zip, then merges the results into a single OrderDetails response. Services that are unavailable contribute an empty Optional rather than failing the whole request.
// OrderHandlers.java
public Mono<ServerResponse> getOrderDetails(ServerRequest serverRequest) {
String orderId = serverRequest.pathVariable("orderId");
Mono<OrderInfo> orderInfo =
orderService.findOrderById(orderId);
Mono<Optional<TicketInfo>> ticketInfo =
kitchenService.findTicketById(orderId)
.map(Optional::of)
.onErrorReturn(Optional.empty());
Mono<Optional<DeliveryInfo>> deliveryInfo =
deliveryService.findDeliveryByOrderId(orderId)
.map(Optional::of)
.onErrorReturn(Optional.empty());
Mono<Optional<BillInfo>> billInfo =
accountingService.findBillByOrderId(orderId)
.map(Optional::of)
.onErrorReturn(Optional.empty());
Mono<Tuple4<OrderInfo,
Optional<TicketInfo>,
Optional<DeliveryInfo>,
Optional<BillInfo>>> combined =
Mono.zip(orderInfo, ticketInfo, deliveryInfo, billInfo);
Mono<OrderDetails> orderDetails =
combined.map(OrderDetails::makeOrderDetails);
return orderDetails
.flatMap(od -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fromObject(od)))
.onErrorResume(OrderNotFoundException.class,
e -> ServerResponse.notFound().build());
}
The four proxies (OrderServiceProxy, KitchenService, DeliveryService, AccountingService) all use WebClient to make non-blocking HTTP calls to their respective services.
Services Composed for a Single Order
| Data | Source Service |
|---|
OrderInfo (status, line items) | Order Service |
TicketInfo (kitchen ticket state) | Kitchen Service |
DeliveryInfo (courier, ETA) | Delivery Service |
BillInfo (authorization state) | Accounting Service |
OrderInfo is required — an OrderNotFoundException returns HTTP 404. The other three are optional; a downstream failure yields an empty field rather than a 500 error.
GraphQL Gateway — ftgo-api-gateway-graphql
The GraphQL gateway is a Node.js service using graphql-tools and express-graphql. It provides a typed schema that lets clients fetch exactly the fields they need and traverse relationships (e.g., fetch an order and its consumer in one request).
Schema
// ftgo-api-gateway-graphql/src/schema.js
const typeDefs = gql`
type Query {
orders(consumerId: Int!): [Order]
order(orderId: Int!): Order
consumer(consumerId: Int!): Consumer
}
type Mutation {
createConsumer(c: ConsumerInfo): Consumer
}
type Order {
orderId: ID
consumerId: Int
consumer: Consumer
restaurant: Restaurant
deliveryInfo: DeliveryInfo
}
type Restaurant {
id: ID
name: String
}
type Consumer {
id: ID
firstName: String
lastName: String
orders: [Order]
}
input ConsumerInfo {
firstName: String
lastName: String
}
type DeliveryInfo {
status: DeliveryStatus
estimatedDeliveryTime: Int
assignedCourier: String
}
enum DeliveryStatus {
PREPARING
READY_FOR_PICKUP
PICKED_UP
DELIVERED
}
`;
Resolvers
Each GraphQL field that requires a downstream call has its own resolver. Related objects are fetched lazily — the consumer field on Order only triggers a Consumer Service call if the client asks for it.
// Root query resolvers
function resolveOrders(_, { consumerId }, context) {
return context.orderServiceProxy.findOrders(consumerId);
}
function resolveOrder(_, { orderId }, context) {
return context.orderServiceProxy.findOrder(orderId);
}
function resolveConsumer(_, { consumerId }, context) {
return context.consumerServiceProxy.findConsumer(consumerId);
}
// Field resolvers on Order
function resolveOrderConsumer({ consumerId }, args, context) {
return context.consumerServiceProxy.findConsumer(consumerId);
}
function resolveOrderRestaurant({ restaurantId }, args, context) {
return context.restaurantServiceProxy.findRestaurant(restaurantId);
}
function resolveOrderDeliveryInfo({ orderId }, args, context) {
return context.deliveryServiceProxy.findDeliveryForOrder(orderId);
}
// Field resolver on Consumer — reuse pre-fetched orders if available
function resolveConsumerOrders({ id, orders }, args, context) {
return orders || context.orderServiceProxy.findOrders(id);
}
const resolvers = {
Query: { orders: resolveOrders, consumer: resolveConsumer, order: resolveOrder },
Mutation: { createConsumer: createConsumer },
Order: { consumer: resolveOrderConsumer, restaurant: resolveOrderRestaurant,
deliveryInfo: resolveOrderDeliveryInfo },
Consumer: { orders: resolveConsumerOrders },
};
Comparison: REST vs GraphQL Gateway
| Aspect | REST Gateway (port 8087) | GraphQL Gateway |
|---|
| Protocol | HTTP/REST | GraphQL over HTTP |
| Implementation | Java, Spring Boot, WebFlux | Node.js, graphql-tools |
| Composition | Fixed — always fetches from 4 services | Flexible — client selects fields |
| Partial failure | Optional.empty() for unavailable services | Resolver returns null for unavailable fields |
| Routing | Spring Cloud Gateway proxy rules | GraphQL resolver dispatch |
| Mutations | Forwarded via proxy routes | createConsumer mutation supported |
Both gateways proxy to the same downstream services. The REST gateway is suitable for traditional REST consumers; the GraphQL gateway allows richer queries such as fetching a consumer together with their full order history in a single request.