Domine GraphQL Federation com tRPC: schema stitching, resolvers distribuídos, gateway unificado e integração seamless entre microserviços para arquiteturas enterprise.
API Unificada: Um único endpoint GraphQL que agrega todos os microserviços, simplificando consumo para o frontend.
Autonomia dos Times: Cada microserviço mantém seu próprio schema, permitindo desenvolvimento independente.
Type Safety Distribuída: Toda a type safety do tRPC é preservada através da federation, mantendo DX excepcional.
Escalabilidade: Gateway otimiza queries distribuídas automaticamente, suportando milhões de requests.
Apollo Federation:
Especificação que permite compor múltiplos schemas GraphQL em um único gateway.
Schema Stitching:
Técnica para combinar schemas de diferentes serviços em uma API unificada.
Subgraph:
Cada microserviço que expõe um schema GraphQL partial que faz parte da federation.
Query Planning:
Otimização automática que distribui queries entre serviços de forma eficiente.
// 📁 gateway/apollo-federation-gateway.ts
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from 'apollo-server-express';
import { Logger } from '@/infrastructure/logging/logger';
// 🎯 Interface para serviços federados
export interface FederatedService {
name: string;
url: string;
schema?: string;
healthCheckPath?: string;
headers?: Record<string, string>;
}
// 🌐 Gateway Configuration
export interface GatewayConfig {
services: FederatedService[];
introspectionInterval?: number;
debug?: boolean;
playground?: boolean;
cors?: boolean;
subscriptions?: boolean;
}
// 🏗️ Apollo Federation Gateway
export class TRPCGraphQLGateway {
private gateway: ApolloGateway;
private server: ApolloServer;
private logger: Logger;
private config: GatewayConfig;
constructor(config: GatewayConfig, logger: Logger) {
this.config = config;
this.logger = logger;
this.setupGateway();
}
// 🔧 Setup Apollo Gateway
private setupGateway(): void {
this.gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: this.config.services.map(service => ({
name: service.name,
url: service.url,
headers: service.headers || {},
})),
introspectionHeaders: {
'Apollo-Require-Preflight': 'true',
},
pollIntervalInMs: this.config.introspectionInterval || 30000,
}),
// 🔍 Service health monitoring
serviceHealthCheck: true,
// 📊 Execution metrics
experimental_approximateQueryPlanStoreMiB: 30,
// 🚨 Error handling personalizado
buildService: ({ url, name }) => {
return {
process: ({ request, context }) => {
// 🔐 Add authentication headers
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': context.authorization || '',
'X-User-ID': context.userId || '',
'X-Organization-ID': context.organizationId || '',
'X-Trace-ID': context.traceId || '',
...request.http?.headers,
},
body: JSON.stringify({
query: request.query,
variables: request.variables,
operationName: request.operationName,
}),
})
.then(response => response.json())
.catch(error => {
this.logger.error('Service request failed', {
service: name,
url,
error: error.message,
traceId: context.traceId,
});
throw error;
});
},
};
},
});
}
// 🚀 Start Gateway Server
async start(port: number = 4000): Promise<void> {
try {
// 🔧 Create Apollo Server
this.server = new ApolloServer({
gateway: this.gateway,
subscriptions: this.config.subscriptions || false,
// 📝 Context creation
context: async ({ req }) => {
return {
authorization: req.headers.authorization,
userId: req.headers['x-user-id'],
organizationId: req.headers['x-organization-id'],
traceId: req.headers['x-trace-id'] || this.generateTraceId(),
};
},
// 🎭 Playground configuration
introspection: this.config.debug || false,
playground: this.config.playground && this.config.debug,
// 📊 Plugins for monitoring
plugins: [
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
console.log('🔍 Operation:', requestContext.operationName);
},
didEncounterErrors(requestContext) {
console.error('❌ GraphQL Errors:', requestContext.errors);
},
};
},
},
],
// 🛡️ Security configuration
cors: this.config.cors || {
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
},
});
await this.server.listen({ port });
this.logger.info('🌐 GraphQL Gateway started', {
port,
services: this.config.services.length,
playground: this.config.playground && this.config.debug,
});
// 📊 Log federated services
for (const service of this.config.services) {
this.logger.info('📡 Federated service registered', {
name: service.name,
url: service.url,
});
}
} catch (error) {
this.logger.error('Failed to start GraphQL Gateway', {
error: error.message,
stack: error.stack,
});
throw error;
}
}
// 🔍 Health Check completo
async healthCheck(): Promise<{
status: string;
services: Array<{
name: string;
status: 'healthy' | 'unhealthy' | 'unknown';
latency?: number;
error?: string;
}>;
}> {
const serviceChecks = await Promise.allSettled(
this.config.services.map(async (service) => {
const start = Date.now();
try {
const healthUrl = service.healthCheckPath
? `${service.url.replace('/graphql', '')}${service.healthCheckPath}`
: `${service.url.replace('/graphql', '')}/health`;
const response = await fetch(healthUrl, {
method: 'GET',
timeout: 5000,
});
const latency = Date.now() - start;
return {
name: service.name,
status: response.ok ? 'healthy' : 'unhealthy',
latency,
};
} catch (error) {
return {
name: service.name,
status: 'unhealthy',
latency: Date.now() - start,
error: error.message,
};
}
})
);
const services = serviceChecks.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
name: this.config.services[index].name,
status: 'unknown',
error: result.reason?.message || 'Unknown error',
};
}
});
const allHealthy = services.every(s => s.status === 'healthy');
return {
status: allHealthy ? 'healthy' : 'degraded',
services,
};
}
// 📁 gateway/production-config.ts
// 🔧 Gateway Configuration para Produção
const gatewayConfig: GatewayConfig = {
services: [
{
name: 'user-service',
url: process.env.USER_SERVICE_URL || 'http://localhost:4001/graphql',
healthCheckPath: '/health',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY || '',
'X-Service-Name': 'gateway',
},
},
{
name: 'organization-service',
url: process.env.ORGANIZATION_SERVICE_URL || 'http://localhost:4002/graphql',
healthCheckPath: '/health',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY || '',
'X-Service-Name': 'gateway',
},
},
{
name: 'billing-service',
url: process.env.BILLING_SERVICE_URL || 'http://localhost:4003/graphql',
healthCheckPath: '/health',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY || '',
'X-Service-Name': 'gateway',
},
},
{
name: 'analytics-service',
url: process.env.ANALYTICS_SERVICE_URL || 'http://localhost:4004/graphql',
healthCheckPath: '/health',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY || '',
'X-Service-Name': 'gateway',
},
},
],
introspectionInterval: 30000, // 30 seconds
debug: process.env.NODE_ENV === 'development',
playground: process.env.NODE_ENV === 'development',
cors: true,
subscriptions: false, // tRPC handles subscriptions
};
// 🚀 Start Gateway with error handling
async function startGateway() {
const logger = new Logger('GraphQL Gateway');
const gateway = new TRPCGraphQLGateway(gatewayConfig, logger);
try {
await gateway.start(parseInt(process.env.GATEWAY_PORT || '4000'));
// 📊 Periodic health checks
setInterval(async () => {
const health = await gateway.healthCheck();
logger.info('🏥 Gateway health check', health);
// 🚨 Alert se serviços estão down
const unhealthyServices = health.services.filter(s => s.status !== 'healthy');
if (unhealthyServices.length > 0) {
logger.error('🚨 Unhealthy services detected', { unhealthyServices });
// Aqui você poderia integrar com alerting (Slack, PagerDuty, etc.)
}
}, 60000); // Every minute
// 🔄 Graceful shutdown
process.on('SIGTERM', async () => {
logger.info('📛 Received SIGTERM, shutting down gracefully');
await gateway.stop();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('📛 Received SIGINT, shutting down gracefully');
await gateway.stop();
process.exit(0);
});
} catch (error) {
logger.error('💥 Failed to start gateway', error);
process.exit(1);
}
}
if (require.main === module) {
startGateway();
}
// 📁 bridge/trpc-graphql-adapter.ts
import { initTRPC } from '@trpc/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'apollo-server-core';
import { GraphQLResolveInfo } from 'graphql';
// 🎯 Types para bridge
export interface TRPCToGraphQLConfig {
serviceName: string;
trpcRouter: any;
context: any;
enableIntrospection?: boolean;
}
export interface GraphQLFieldMapping {
trpcProcedure: string;
graphqlField: string;
inputMapping?: Record<string, string>;
outputMapping?: Record<string, string>;
}
// 🌉 tRPC to GraphQL Adapter
export class TRPCGraphQLBridge {
private config: TRPCToGraphQLConfig;
private fieldMappings: Map<string, GraphQLFieldMapping> = new Map();
constructor(config: TRPCToGraphQLConfig) {
this.config = config;
}
// 📝 Register field mapping
addFieldMapping(mapping: GraphQLFieldMapping): void {
this.fieldMappings.set(mapping.graphqlField, mapping);
}
// 🏗️ Build GraphQL Schema from tRPC Router
buildSchema(): any {
const typeDefs = this.generateTypeDefs();
const resolvers = this.generateResolvers();
return buildSubgraphSchema([
{
typeDefs,
resolvers,
},
]);
}
// 📞 Call tRPC Procedure Helper
private async callTRPCProcedure(
procedurePath: string,
input: any,
context: any
): Promise<any> {
try {
// 🎯 Navigate to tRPC procedure
const pathParts = procedurePath.split('.');
let procedure = context.trpc;
for (const part of pathParts) {
procedure = procedure[part];
if (!procedure) {
throw new Error(`tRPC procedure not found: ${procedurePath}`);
}
}
// 📞 Execute procedure
const result = await procedure(input);
context.logger?.debug('tRPC procedure executed', {
procedure: procedurePath,
input,
success: true,
});
return result;
} catch (error) {
context.logger?.error('tRPC procedure failed', {
procedure: procedurePath,
input,
error: error.message,
});
throw error;
}
}
}
// 📄 Generate GraphQL Type Definitions
private generateTypeDefs(): any {
return gql`
# 👤 User Entity (Federated)
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
role: UserRole!
organizationId: ID
isActive: Boolean!
createdAt: String!
updatedAt: String!
metadata: JSON
# 🔗 Federation: Relations to other services
organization: Organization @provides(fields: "name")
billingProfile: BillingProfile @provides(fields: "plan")
analytics: UserAnalytics @provides(fields: "lastActive")
}
# 🏢 Organization Entity (Federated)
type Organization @key(fields: "id") {
id: ID!
name: String!
plan: String!
isActive: Boolean!
createdAt: String!
# 🔗 Relations
users: [User!]! @provides(fields: "id email name")
owner: User! @provides(fields: "id name email")
billing: BillingProfile @provides(fields: "plan status")
}
# 💳 Billing Profile (External reference)
type BillingProfile @key(fields: "organizationId") @extends {
organizationId: ID! @external
plan: String!
status: BillingStatus!
nextBillingDate: String
usage: UsageMetrics
}
# 📊 User Analytics (External reference)
type UserAnalytics @key(fields: "userId") @extends {
userId: ID! @external
lastActive: String
sessionsCount: Int!
averageSessionDuration: Int!
featuresUsed: [String!]!
}
# 🎯 Enums
enum UserRole {
USER
ADMIN
SUPER_ADMIN
}
enum BillingStatus {
ACTIVE
PAST_DUE
CANCELED
TRIAL
}
# 📊 Custom Scalars
scalar JSON
scalar DateTime
# 📝 Input Types
input CreateUserInput {
email: String!
name: String!
role: UserRole = USER
organizationId: ID
metadata: JSON
}
input UpdateUserInput {
id: ID!
email: String
name: String
role: UserRole
}
input UserFilters {
organizationId: ID
role: UserRole
isActive: Boolean
search: String
}
input PaginationInput {
page: Int = 1
limit: Int = 20
sortBy: String = "createdAt"
sortOrder: SortOrder = DESC
}
enum SortOrder {
ASC
DESC
}
# 📋 Response Types
type UserConnection {
nodes: [User!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo {
currentPage: Int!
totalPages: Int!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
# 📋 Queries
type Query {
# 👤 User Queries
user(id: ID!): User
users(filters: UserFilters, pagination: PaginationInput): UserConnection!
userStats(organizationId: ID): UserStats!
# 🔍 Search
searchUsers(query: String!, organizationId: ID): [User!]!
}
# ✏️ Mutations
type Mutation {
# 👤 User Management
createUser(input: CreateUserInput!): User!
updateUser(input: UpdateUserInput!): User!
deactivateUser(id: ID!, reason: String!): Boolean!
# 🔄 Bulk Operations
bulkUpdateUsers(userIds: [ID!]!, updates: UpdateUserInput!): [User!]!
}
# 📡 Subscriptions (handled by tRPC)
type Subscription {
userCreated(organizationId: ID): User!
userUpdated(userId: ID): User!
userDeactivated(organizationId: ID): User!
}
`;
}
// 🔧 Generate GraphQL Resolvers
private generateResolvers(): any {
return {
// 🎯 Entity resolvers
User: {
// 🔑 Federation key resolver
__resolveReference: async (reference: { id: string }, context: any) => {
try {
// 📞 Call tRPC procedure
const user = await this.callTRPCProcedure(
'user.queries.getById',
{ id: reference.id },
context
);
return user;
} catch (error) {
context.logger?.error('Failed to resolve User reference', {
userId: reference.id,
error: error.message,
});
return null;
}
},
// 🔗 Relation resolvers
organization: async (user: any, args: any, context: any) => {
if (!user.organizationId) return null;
// 🎯 This will be resolved by organization service
return { __typename: 'Organization', id: user.organizationId };
},
billingProfile: async (user: any, args: any, context: any) => {
if (!user.organizationId) return null;
// 🎯 This will be resolved by billing service
return { __typename: 'BillingProfile', organizationId: user.organizationId };
},
analytics: async (user: any, args: any, context: any) => {
// 🎯 This will be resolved by analytics service
return { __typename: 'UserAnalytics', userId: user.id };
},
},
Organization: {
__resolveReference: async (reference: { id: string }, context: any) => {
try {
const organization = await this.callTRPCProcedure(
'organization.queries.getById',
{ id: reference.id },
context
);
return organization;
} catch (error) {
context.logger?.error('Failed to resolve Organization reference', {
organizationId: reference.id,
error: error.message,
});
return null;
}
},
users: async (organization: any, args: any, context: any) => {
const users = await this.callTRPCProcedure(
'user.queries.list',
{
organizationId: organization.id,
page: 1,
limit: 100,
},
context
);
return users.users;
},
owner: async (organization: any, args: any, context: any) => {
const owner = await this.callTRPCProcedure(
'user.queries.getById',
{ id: organization.ownerId },
context
);
return owner;
},
},
// 📋 Query resolvers
Query: {
user: async (parent: any, args: { id: string }, context: any) => {
return this.callTRPCProcedure('user.queries.getById', args, context);
},
users: async (parent: any, args: any, context: any) => {
const result = await this.callTRPCProcedure('user.queries.list', args, context);
return {
nodes: result.users,
totalCount: result.total,
pageInfo: {
currentPage: result.page,
totalPages: result.totalPages,
hasNextPage: result.page < result.totalPages,
hasPreviousPage: result.page > 1,
},
};
},
searchUsers: async (parent: any, args: any, context: any) => {
return this.callTRPCProcedure('user.queries.search', args, context);
},
},
// ✏️ Mutation resolvers
Mutation: {
createUser: async (parent: any, args: { input: any }, context: any) => {
return this.callTRPCProcedure('user.commands.create', args.input, context);
},
updateUser: async (parent: any, args: { input: any }, context: any) => {
return this.callTRPCProcedure('user.commands.update', args.input, context);
},
deactivateUser: async (parent: any, args: { id: string; reason: string }, context: any) => {
const result = await this.callTRPCProcedure('user.commands.deactivate', args, context);
return result.success;
},
},
// 📡 Subscription resolvers (delegate to tRPC)
Subscription: {
userCreated: {
// 🔗 tRPC subscription bridge
subscribe: async (parent: any, args: any, context: any) => {
return context.trpc.user.subscriptions.userCreated(args);
},
},
userUpdated: {
subscribe: async (parent: any, args: any, context: any) => {
return context.trpc.user.subscriptions.userUpdated(args);
},
},
},
};
}
// 📁 bridge/user-service-bridge.ts
// 🎯 User Service Bridge Implementation
import { TRPCGraphQLBridge } from './trpc-graphql-adapter';
import { userCQRSRouter } from '@/server/routers/user-cqrs-router';
// 🔧 Create User Service Bridge
export function createUserServiceBridge() {
const bridge = new TRPCGraphQLBridge({
serviceName: 'user-service',
trpcRouter: userCQRSRouter,
context: {}, // Will be populated by Apollo
enableIntrospection: process.env.NODE_ENV === 'development',
});
// 📝 Register field mappings
bridge.addFieldMapping({
trpcProcedure: 'user.queries.getById',
graphqlField: 'user',
inputMapping: { id: 'id' },
});
bridge.addFieldMapping({
trpcProcedure: 'user.queries.list',
graphqlField: 'users',
inputMapping: {
filters: 'filters',
pagination: 'pagination',
},
});
bridge.addFieldMapping({
trpcProcedure: 'user.commands.create',
graphqlField: 'createUser',
inputMapping: { input: 'input' },
});
bridge.addFieldMapping({
trpcProcedure: 'user.commands.update',
graphqlField: 'updateUser',
inputMapping: { input: 'input' },
});
bridge.addFieldMapping({
trpcProcedure: 'user.commands.deactivate',
graphqlField: 'deactivateUser',
inputMapping: { id: 'id', reason: 'reason' },
});
return bridge.buildSchema();
}
// 📁 services/user-service.ts
// 🚀 User Service com Federation
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { createUserServiceBridge } from './bridge/user-service-bridge';
import { createContext } from './context';
async function startUserService() {
const app = express();
// 🌉 Create federated schema
const schema = createUserServiceBridge();
// 🚀 Apollo Server setup
const server = new ApolloServer({
schema,
context: createContext,
plugins: [
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
console.log('🎯 User Service Operation:', requestContext.operationName);
},
};
},
},
],
});
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
// 🏥 Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'user-service',
timestamp: new Date().toISOString(),
});
});
const port = process.env.USER_SERVICE_PORT || 4001;
app.listen(port, () => {
console.log(`🚀 User Service running on http://localhost:${port}/graphql`);
console.log(`🏥 Health check: http://localhost:${port}/health`);
});
}
if (require.main === module) {
startUserService().catch(console.error);
}
// 📁 stitching/schema-composer.ts
import { stitchSchemas } from '@graphql-tools/stitch';
import { introspectSchema, wrapSchema } from '@graphql-tools/wrap';
import { fetch } from 'cross-fetch';
import { print } from 'graphql';
import { ExecutionResult } from 'graphql';
// 🎯 Service Configuration
export interface ServiceConfig {
name: string;
endpoint: string;
headers?: Record<string, string>;
transforms?: any[];
merge?: Record<string, any>;
}
// 🧩 Schema Stitching Composer
export class SchemaStitchingComposer {
private services: ServiceConfig[];
private stitchedSchema: any = null;
private logger: Logger;
constructor(services: ServiceConfig[], logger: Logger) {
this.services = services;
this.logger = logger;
}
// 🏗️ Compose unified schema
async composeSchema(): Promise<any> {
try {
const subschemas = await Promise.all(
this.services.map(service => this.createSubschema(service))
);
this.stitchedSchema = stitchSchemas({
subschemas,
// 🔗 Cross-service type merging
typeMerging: {
User: {
// 🎯 Merge User type from multiple services
fieldName: 'user',
selectionSet: '{ id }',
key: ({ id }: { id: string }) => id,
argsFromKeys: (keys: string[]) => ({ ids: keys }),
},
Organization: {
fieldName: 'organization',
selectionSet: '{ id }',
key: ({ id }: { id: string }) => id,
argsFromKeys: (keys: string[]) => ({ ids: keys }),
},
},
// 🔄 Merge configurations
mergeTypes: true,
// 🛡️ Error handling
onTypeConflict: (left, right, info) => {
this.logger.warn('Type conflict detected', {
leftType: left.name,
rightType: right.name,
info: info?.message,
});
return left; // Prefer left schema
},
});
this.logger.info('✅ Schema stitching completed', {
servicesCount: this.services.length,
typesCount: Object.keys(this.stitchedSchema.getTypeMap()).length,
});
return this.stitchedSchema;
} catch (error) {
this.logger.error('❌ Schema stitching failed', {
error: error.message,
stack: error.stack,
});
throw error;
}
}
// 🔧 Create subschema for service
private async createSubschema(service: ServiceConfig): Promise<any> {
try {
// 🔍 Introspect remote schema
const schema = await introspectSchema(
async (query) => this.executeRemoteQuery(service, query)
);
// 🌯 Wrap schema with service-specific logic
const wrappedSchema = wrapSchema({
schema,
executor: async ({ document, variables, context, info }) => {
const query = print(document);
return this.executeRemoteQuery(service, query, variables, context);
},
// 🔄 Schema transforms
transforms: service.transforms || [],
});
this.logger.info('📡 Subschema created', {
service: service.name,
endpoint: service.endpoint,
typesCount: Object.keys(schema.getTypeMap()).length,
});
return {
schema: wrappedSchema,
merge: service.merge || {},
batch: true, // Enable query batching
// 🔐 Context transformation
createProxyingResolver: ({ subschemaConfig, operation, transformedSchema }) => {
return async (root, args, context, info) => {
// 🎯 Add service-specific context
const serviceContext = {
...context,
service: service.name,
traceId: context.traceId || this.generateTraceId(),
};
return operation({
root,
args,
context: serviceContext,
info,
schema: transformedSchema,
});
};
},
};
} catch (error) {
this.logger.error('Failed to create subschema', {
service: service.name,
endpoint: service.endpoint,
error: error.message,
});
throw error;
}
}
// 📞 Execute remote GraphQL query
private async executeRemoteQuery(
service: ServiceConfig,
query: string,
variables?: any,
context?: any
): Promise<ExecutionResult> {
try {
const response = await fetch(service.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'GraphQL-Schema-Stitching/1.0',
...service.headers,
...(context?.authorization && {
'Authorization': context.authorization,
}),
...(context?.traceId && {
'X-Trace-ID': context.traceId,
}),
},
body: JSON.stringify({
query,
variables,
operationName: null,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
// 📊 Log successful request
this.logger.debug('Remote query executed', {
service: service.name,
operationType: query.includes('mutation') ? 'mutation' : 'query',
hasErrors: !!result.errors,
variables,
});
return result;
} catch (error) {
this.logger.error('Remote query failed', {
service: service.name,
endpoint: service.endpoint,
error: error.message,
query: query.substring(0, 200), // Log first 200 chars
});
return {
errors: [{
message: `Service ${service.name} is unavailable: ${error.message}`,
extensions: {
code: 'SERVICE_UNAVAILABLE',
service: service.name,
},
}],
};
}
}
// 📁 stitching/distributed-resolvers.ts
// 🎯 Distributed Resolvers for Cross-Service Relations
export class DistributedResolverManager {
private services: Map<string, ServiceConfig> = new Map();
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger;
}
// 📝 Register service
registerService(config: ServiceConfig): void {
this.services.set(config.name, config);
this.logger.info('Service registered for distributed resolvers', {
service: config.name,
endpoint: config.endpoint,
});
}
// 🔗 Create cross-service resolvers
createDistributedResolvers(): Record<string, any> {
return {
// 👤 User distributed resolvers
User: {
// 🏢 Resolve organization from organization-service
organization: async (user: any, args: any, context: any) => {
if (!user.organizationId) return null;
return this.resolveFromService('organization-service', {
query: `
query GetOrganization($id: ID!) {
organization(id: $id) {
id
name
plan
isActive
createdAt
}
}
`,
variables: { id: user.organizationId },
context,
});
},
// 💳 Resolve billing from billing-service
billingProfile: async (user: any, args: any, context: any) => {
if (!user.organizationId) return null;
return this.resolveFromService('billing-service', {
query: `
query GetBillingProfile($organizationId: ID!) {
billingProfile(organizationId: $organizationId) {
organizationId
plan
status
nextBillingDate
usage {
apiCalls
storageUsed
bandwidthUsed
}
}
}
`,
variables: { organizationId: user.organizationId },
context,
});
},
// 📊 Resolve analytics from analytics-service
analytics: async (user: any, args: any, context: any) => {
return this.resolveFromService('analytics-service', {
query: `
query GetUserAnalytics($userId: ID!) {
userAnalytics(userId: $userId) {
userId
lastActive
sessionsCount
averageSessionDuration
featuresUsed
}
}
`,
variables: { userId: user.id },
context,
fallback: {
userId: user.id,
lastActive: null,
sessionsCount: 0,
averageSessionDuration: 0,
featuresUsed: [],
},
});
},
},
// 🏢 Organization distributed resolvers
Organization: {
// 👥 Resolve users from user-service
users: async (organization: any, args: any, context: any) => {
const result = await this.resolveFromService('user-service', {
query: `
query GetOrganizationUsers($organizationId: ID!, $pagination: PaginationInput) {
users(
filters: { organizationId: $organizationId }
pagination: $pagination
) {
nodes {
id
email
name
role
isActive
createdAt
}
totalCount
}
}
`,
variables: {
organizationId: organization.id,
pagination: args.pagination || { page: 1, limit: 50 }
},
context,
});
return result?.users?.nodes || [];
},
// 👑 Resolve owner from user-service
owner: async (organization: any, args: any, context: any) => {
if (!organization.ownerId) return null;
return this.resolveFromService('user-service', {
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
email
name
role
isActive
}
}
`,
variables: { id: organization.ownerId },
context,
});
},
},
};
}
// 📞 Resolve from specific service
private async resolveFromService(
serviceName: string,
options: {
query: string;
variables?: any;
context: any;
fallback?: any;
}
): Promise<any> {
const service = this.services.get(serviceName);
if (!service) {
this.logger.error('Service not found for distributed resolver', {
service: serviceName,
availableServices: Array.from(this.services.keys()),
});
return options.fallback || null;
}
try {
const response = await fetch(service.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...service.headers,
...(options.context?.authorization && {
'Authorization': options.context.authorization,
}),
...(options.context?.traceId && {
'X-Trace-ID': options.context.traceId,
}),
},
body: JSON.stringify({
query: options.query,
variables: options.variables,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.errors) {
this.logger.warn('GraphQL errors in distributed resolver', {
service: serviceName,
errors: result.errors,
variables: options.variables,
});
return options.fallback || null;
}
// 🎯 Extract the first field from data
const dataKeys = Object.keys(result.data || {});
return dataKeys.length > 0 ? result.data[dataKeys[0]] : options.fallback;
} catch (error) {
this.logger.error('Distributed resolver failed', {
service: serviceName,
error: error.message,
variables: options.variables,
});
return options.fallback || null;
}
}
// 📁 stitching/production-setup.ts
// 🚀 Production Schema Stitching Setup
export async function createProductionStitchingGateway() {
const logger = new Logger('Schema Stitching');
const services: ServiceConfig[] = [
{
name: 'user-service',
endpoint: process.env.USER_SERVICE_URL + '/graphql',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY,
'X-Service-Version': '1.0.0',
},
},
{
name: 'organization-service',
endpoint: process.env.ORGANIZATION_SERVICE_URL + '/graphql',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY,
'X-Service-Version': '1.0.0',
},
},
{
name: 'billing-service',
endpoint: process.env.BILLING_SERVICE_URL + '/graphql',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY,
'X-Service-Version': '1.0.0',
},
},
{
name: 'analytics-service',
endpoint: process.env.ANALYTICS_SERVICE_URL + '/graphql',
headers: {
'X-API-Key': process.env.INTERNAL_API_KEY,
'X-Service-Version': '1.0.0',
},
},
];
// 🏗️ Create schema composer
const composer = new SchemaStitchingComposer(services, logger);
const resolverManager = new DistributedResolverManager(logger);
// 📝 Register services for distributed resolvers
services.forEach(service => resolverManager.registerService(service));
// 🧩 Compose unified schema
const stitchedSchema = await composer.composeSchema();
// 🔗 Add distributed resolvers
const distributedResolvers = resolverManager.createDistributedResolvers();
// 🔄 Merge with existing resolvers
const finalSchema = stitchSchemas({
subschemas: [{ schema: stitchedSchema }],
resolvers: distributedResolvers,
});
logger.info('🎉 Production stitching gateway ready', {
servicesCount: services.length,
hasDistributedResolvers: Object.keys(distributedResolvers).length > 0,
});
return finalSchema;
}
// 📁 client/unified-client.ts
import { createTRPCNext } from '@trpc/next';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import type { AppRouter } from '@/server/routers/_app';
// 🎯 Unified Client Configuration
interface UnifiedClientConfig {
trpcEndpoint: string;
graphqlEndpoint: string;
wsEndpoint?: string;
apiKey?: string;
enableBatching?: boolean;
enableCache?: boolean;
retryAttempts?: number;
}
// 🔗 tRPC Client Setup
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: process.env.NEXT_PUBLIC_TRPC_ENDPOINT || 'http://localhost:3000/api/trpc',
// 📝 Headers for authentication
headers: async () => {
const token = typeof window !== 'undefined'
? localStorage.getItem('auth-token')
: ctx?.req?.headers?.authorization;
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
'X-Client-Type': 'web',
'X-Client-Version': process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
};
},
// 🔄 Request/Response transformation
fetch: async (url, options) => {
const startTime = Date.now();
try {
const response = await fetch(url, {
...options,
timeout: 30000, // 30 second timeout
});
// 📊 Log request metrics
console.debug('tRPC Request completed', {
url,
method: options?.method,
status: response.status,
duration: Date.now() - startTime,
});
return response;
} catch (error) {
console.error('tRPC Request failed', {
url,
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
},
}),
],
// 🔄 React Query configuration
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 10, // 10 minutes
retry: 3,
refetchOnWindowFocus: false,
refetchOnMount: false,
},
mutations: {
retry: 1,
},
},
},
};
},
ssr: false,
});
// 🌐 GraphQL Apollo Client Setup
const createApolloClient = (config: UnifiedClientConfig) => {
// 🔗 HTTP Link
const httpLink = createHttpLink({
uri: config.graphqlEndpoint,
credentials: 'include',
fetch: async (uri, options) => {
const startTime = Date.now();
try {
const response = await fetch(uri, options);
console.debug('GraphQL Request completed', {
uri,
status: response.status,
duration: Date.now() - startTime,
});
return response;
} catch (error) {
console.error('GraphQL Request failed', {
uri,
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
},
});
// 🔐 Auth Link
const authLink = setContext(async (_, { headers }) => {
const token = typeof window !== 'undefined'
? localStorage.getItem('auth-token')
: null;
const organizationId = typeof window !== 'undefined'
? localStorage.getItem('current-organization-id')
: null;
return {
headers: {
...headers,
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
...(organizationId && { 'X-Organization-ID': organizationId }),
...(config.apiKey && { 'X-API-Key': config.apiKey }),
'X-Client-Type': 'web',
'X-Client-Version': process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
'X-Trace-ID': `trace_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
},
};
});
// 🚨 Error Link
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error('GraphQL Error', {
message,
locations,
path,
extensions,
operation: operation.operationName,
variables: operation.variables,
});
// 🔐 Handle authentication errors
if (extensions?.code === 'UNAUTHENTICATED') {
if (typeof window !== 'undefined') {
localStorage.removeItem('auth-token');
window.location.href = '/login';
}
}
});
}
if (networkError) {
console.error('GraphQL Network Error', {
error: networkError.message,
operation: operation.operationName,
variables: operation.variables,
});
}
});
// 🔄 Retry Link
const retryLink = new RetryLink({
delay: {
initial: 300,
max: Infinity,
jitter: true,
},
attempts: {
max: config.retryAttempts || 3,
retryIf: (error, _operation) => {
return !!error && !error.message.includes('UNAUTHENTICATED');
},
},
});
// 💾 Cache configuration
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
fields: {
analytics: {
merge: true,
},
organization: {
merge: true,
},
billingProfile: {
merge: true,
},
},
},
Organization: {
keyFields: ['id'],
fields: {
users: {
keyArgs: ['filters'],
merge(existing = [], incoming = []) {
return [...existing, ...incoming];
},
},
billing: {
merge: true,
},
},
},
UserConnection: {
keyFields: false,
fields: {
nodes: {
merge: false,
},
},
},
BillingProfile: {
keyFields: ['organizationId'],
fields: {
usage: {
merge: true,
},
},
},
UserAnalytics: {
keyFields: ['userId'],
fields: {
featuresUsed: {
merge: false,
},
},
},
},
// 🔧 Custom cache resolver
possibleTypes: {
Node: ['User', 'Organization', 'BillingProfile'],
},
});
return new ApolloClient({
link: from([errorLink, retryLink, authLink, httpLink]),
cache,
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
fetchPolicy: config.enableCache ? 'cache-first' : 'network-only',
},
query: {
errorPolicy: 'all',
fetchPolicy: config.enableCache ? 'cache-first' : 'network-only',
},
mutate: {
errorPolicy: 'all',
},
},
connectToDevTools: process.env.NODE_ENV === 'development',
});
// 🔗 Unified Client Hook
export function useUnifiedClient(config?: Partial<UnifiedClientConfig>) {
const defaultConfig: UnifiedClientConfig = {
trpcEndpoint: process.env.NEXT_PUBLIC_TRPC_ENDPOINT || 'http://localhost:3000/api/trpc',
graphqlEndpoint: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
enableBatching: true,
enableCache: true,
retryAttempts: 3,
...config,
};
const apolloClient = createApolloClient(defaultConfig);
return {
// 🎯 tRPC Client
trpc,
// 🌐 GraphQL Client
graphql: apolloClient,
// 🔄 Unified operations
operations: {
// 📊 Get user with full analytics (GraphQL)
async getUserWithAnalytics(userId: string) {
const { data } = await apolloClient.query({
query: gql`
query GetUserWithAnalytics($userId: ID!) {
user(id: $userId) {
id
email
name
role
isActive
createdAt
organization {
id
name
plan
}
analytics {
lastActive
sessionsCount
averageSessionDuration
featuresUsed
}
billingProfile {
plan
status
usage {
apiCalls
storageUsed
bandwidthUsed
}
}
}
}
`,
variables: { userId },
});
return data.user;
},
// ⚡ Create user (tRPC para speed)
async createUser(input: any) {
return trpc.user.commands.create.mutate(input);
},
// 📋 List users with relations (GraphQL para relacionamentos)
async listUsersWithRelations(filters?: any, pagination?: any) {
const { data } = await apolloClient.query({
query: gql`
query ListUsersWithRelations($filters: UserFilters, $pagination: PaginationInput) {
users(filters: $filters, pagination: $pagination) {
nodes {
id
email
name
role
isActive
organization {
id
name
plan
}
analytics {
lastActive
sessionsCount
}
}
totalCount
pageInfo {
currentPage
totalPages
hasNextPage
}
}
}
`,
variables: { filters, pagination },
});
return data.users;
},
// 📊 Get organization dashboard (GraphQL para dados relacionais complexos)
async getOrganizationDashboard(organizationId: string) {
const { data } = await apolloClient.query({
query: gql`
query GetOrganizationDashboard($organizationId: ID!) {
organization(id: $organizationId) {
id
name
plan
isActive
createdAt
users {
id
email
name
role
isActive
analytics {
lastActive
sessionsCount
}
}
billing {
plan
status
nextBillingDate
usage {
apiCalls
storageUsed
bandwidthUsed
features {
feature
usageCount
lastUsed
}
}
}
}
userStats(organizationId: $organizationId) {
total
active
inactive
byRole {
role
count
}
recentSignups
}
}
`,
variables: { organizationId },
});
return {
organization: data.organization,
stats: data.userStats,
};
},
// ⚡ Real-time user events (tRPC subscription)
subscribeToUserEvents(organizationId: string, onUpdate: (user: any) => void) {
return trpc.user.subscriptions.userUpdated.subscribe(
{ organizationId },
{
onData: onUpdate,
onError: (error) => {
console.error('User subscription error:', error);
},
}
);
},
// 🔄 Hybrid search (tRPC para performance, GraphQL para relações)
async hybridUserSearch(query: string, includeRelations = false) {
if (includeRelations) {
// 🌐 Use GraphQL for complex relations
const { data } = await apolloClient.query({
query: gql`
query SearchUsersWithRelations($query: String!) {
searchUsers(query: $query) {
id
email
name
role
organization {
id
name
plan
}
analytics {
lastActive
sessionsCount
}
}
}
`,
variables: { query },
});
return data.searchUsers;
} else {
// ⚡ Use tRPC for fast simple searches
return trpc.user.queries.search.query({ query });
}
},
// 📁 client/hooks/use-unified-mutations.ts
// 🔄 Unified Mutations Hook
import { useMutation, useQueryClient } from '@apollo/client';
import { gql } from '@apollo/client';
export function useUnifiedMutations() {
const queryClient = useQueryClient();
const { trpc, graphql } = useUnifiedClient();
return {
// 👤 User mutations
createUser: useMutation(
gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
name
role
organizationId
isActive
createdAt
}
}
`,
{
onSuccess: (data) => {
// 🔄 Invalidate related queries
queryClient.invalidateQueries(['users']);
queryClient.invalidateQueries(['userStats']);
// 🔄 Also invalidate tRPC cache
trpc.user.queries.list.invalidate();
console.log('User created successfully', { userId: data.createUser.id });
},
}
),
updateUser: trpc.user.commands.changeEmail.useMutation({
onSuccess: () => {
// 🔄 Invalidate both tRPC and GraphQL caches
trpc.user.queries.getById.invalidate();
trpc.user.queries.list.invalidate();
queryClient.invalidateQueries(['user']);
queryClient.invalidateQueries(['users']);
},
}),
// 🏢 Organization mutations
updateOrganization: trpc.organization.commands.update.useMutation({
onSuccess: () => {
queryClient.invalidateQueries(['organization']);
queryClient.invalidateQueries(['organizationDashboard']);
},
}),
// 🔄 Bulk operations (preferir GraphQL para bulk)
bulkUpdateUsers: useMutation(
gql`
mutation BulkUpdateUsers($userIds: [ID!]!, $updates: UpdateUserInput!) {
bulkUpdateUsers(userIds: $userIds, updates: $updates) {
id
name
role
isActive
}
}
`,
{
onSuccess: () => {
queryClient.invalidateQueries(['users']);
queryClient.invalidateQueries(['userStats']);
trpc.user.queries.list.invalidate();
},
}
),
};
}
// 📁 client/hooks/use-federation-queries.ts
// 🔍 Federation Queries Hook
export function useFederationQueries() {
const { trpc, graphql } = useUnifiedClient();
return {
// 📊 User with full context (GraphQL federation)
useUserWithContext: (userId: string) => {
return useQuery({
queryKey: ['userWithContext', userId],
queryFn: async () => {
const { data } = await graphql.query({
query: gql`
query GetUserWithContext($userId: ID!) {
user(id: $userId) {
id
email
name
role
isActive
organization {
id
name
plan
billing {
status
nextBillingDate
}
}
analytics {
lastActive
sessionsCount
averageSessionDuration
featuresUsed
}
billingProfile {
plan
status
usage {
apiCalls
storageUsed
bandwidthUsed
}
}
}
}
`,
variables: { userId },
});
return data.user;
},
enabled: !!userId,
});
},
// ⚡ Fast user lookup (tRPC)
useFastUser: trpc.user.queries.getById.useQuery,
// 📋 Users list with pagination (hybrid)
useUsersList: (filters: any, useRelations = true) => {
if (useRelations) {
return useQuery({
queryKey: ['usersWithRelations', filters],
queryFn: async () => {
const { data } = await graphql.query({
query: gql`
query GetUsersWithRelations($filters: UserFilters, $pagination: PaginationInput) {
users(filters: $filters, pagination: $pagination) {
nodes {
id
email
name
role
isActive
organization {
id
name
plan
}
analytics {
lastActive
sessionsCount
}
}
totalCount
pageInfo {
currentPage
totalPages
hasNextPage
}
}
}
`,
variables: { filters, pagination: { page: 1, limit: 20 } },
});
return data.users;
},
});
} else {
return trpc.user.queries.list.useQuery(filters);
}
},
};
}
Query Planning:Apollo Gateway otimiza automaticamente queries distribuídas entre serviços.
Caching Strategy:Cache em múltiplas camadas: Apollo Cache + tRPC Cache + Service Cache.
Error Boundaries:Falhas em um serviço não afetam outros, com fallbacks automáticos.
Tracing Distribuído:Trace IDs permitem rastrear requests através de todos os serviços.
Agora você domina GraphQL Federation e pode criar APIs unificadas enterprise-level!