GraphQL Architecture Guide: Server Setup, Subscriptions & Apollo Federation
GraphQL architecture covers production concerns beyond the schema — server configuration, real-time subscriptions, federated services, and security.
What You’ll Learn
- Apollo Server configuration and middleware
- Subscriptions for real-time data with WebSockets
- Schema federation for distributed GraphQL
- Security: depth limiting, cost analysis, auth
- Performance: caching, persisted queries, CDN
Why Architecture Matters
A GraphQL API in development is different from one handling production traffic. Without proper architecture, you’ll face performance issues, security vulnerabilities, and scaling problems. DodaTech’s Durga Antivirus Pro uses a federated GraphQL architecture — one gateway combines schemas from the devices service, threats service, and users service — enabling independent team deployment while providing a unified API.
flowchart LR
A["Client"] --> B["GraphQL Gateway"]
B --> C["Devices\nService"]
B --> D["Threats\nService"]
B --> E["Users\nService"]
C --> F["Firestore"]
D --> G["External Threat\nIntelligence API"]
E --> H["Auth0 / Firebase"]
style B fill:#dbeafe,stroke:#2563eb
style C fill:#fef3c7,stroke:#d97706
style D fill:#fef3c7,stroke:#d97706
style E fill:#fef3c7,stroke:#d97706
Apollo Server Configuration
const { ApolloServer } = require('apollo-server');
const { ApolloServerPluginCacheControl } = require('apollo-server-core');
const responseCachePlugin = require('apollo-server-plugin-response-cache');
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true, // CSRF protection
cache: 'bounded', // Bounded in-memory cache
introspection: process.env.NODE_ENV !== 'production', // Disable in production
persistedQueries: true, // Persisted query support
plugins: [
ApolloServerPluginCacheControl({ defaultMaxAge: 300 }),
responseCachePlugin(),
],
context: async ({ req }) => {
const user = await authenticate(req);
return { user, db };
},
// Limit query depth
validationRules: [depthLimit(7)],
});
server.listen(4000).then(({ url }) => console.log(`Server at ${url}`));Subscriptions for Real-time Data
Subscriptions use WebSockets to push data from server to client:
Schema
type Subscription {
threatDetected(deviceId: ID): Threat!
deviceStatusChanged: Device!
}Server
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const resolvers = {
Subscription: {
threatDetected: {
subscribe: (parent, { deviceId }) => {
// Filter by device ID if provided
const channel = deviceId
? `THREAT_DETECTED_${deviceId}`
: 'THREAT_DETECTED_ALL';
return pubsub.asyncIterator(channel);
},
},
},
Mutation: {
reportThreat: async (parent, { input }, context) => {
const threat = await createThreat(input);
// Publish to subscribers
pubsub.publish(`THREAT_DETECTED_${threat.deviceId}`, {
threatDetected: threat,
});
pubsub.publish('THREAT_DETECTED_ALL', {
threatDetected: threat,
});
return threat;
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
context,
subscriptions: {
path: '/subscriptions',
onConnect: (connectionParams) => {
// Verify auth token from connection params
if (connectionParams.authToken) {
return { user: verifyToken(connectionParams.authToken) };
}
throw new Error('Auth required for subscriptions');
},
},
});Client
import { ApolloClient, InMemoryCache, gql, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
// HTTP link for queries/mutations
const httpLink = createHttpLink({ uri: 'http://localhost:4000' });
// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: { connectionParams: { authToken: 'bearer-token' } },
});
// Split based on operation type
const link = split(
({ query }) => {
const def = getMainDefinition(query);
return def.kind === 'OperationDefinition' && def.operation === 'subscription';
},
wsLink,
httpLink
);
const client = new ApolloClient({ link, cache: new InMemoryCache() });
// Subscribe to real-time threats
client.subscribe({
query: gql`subscription {
threatDetected(deviceId: "dev-001") { id name severity }
}`,
}).subscribe({
next({ data }) {
console.log('New threat:', data.threatDetected.name);
showNotification(data.threatDetected);
},
});Schema Federation
Federation splits a GraphQL schema across multiple services:
// Gateway
const { ApolloGateway } = require('@apollo/gateway');
const { ApolloServer } = require('apollo-server');
const gateway = new ApolloGateway({
serviceList: [
{ name: 'devices', url: 'http://devices-service:4001' },
{ name: 'threats', url: 'http://threats-service:4002' },
{ name: 'users', url: 'http://users-service:4003' },
],
});
const server = new ApolloServer({ gateway, subscriptions: false });
server.listen(4000);// Devices service schema (partial)
type Device @key(fields: "id") {
id: ID!
name: String!
os: String!
userId: ID!
}
// Users service schema (extends Device)
type Device @key(fields: "id") {
id: ID!
user: User!
}
extend type User @key(fields: "id") {
id: ID! @external
devices: [Device!]!
}Security Patterns
const { ApolloServer } = require('apollo-server');
const { costAnalysis } = require('graphql-cost-analysis');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7), // Max query depth
costAnalysis({
maximumCost: 1000, // Max query cost
defaultCost: 1,
costMap: {
// Set higher costs for expensive fields
"Device.threats": 10,
"User.devices": 5,
},
}),
],
});Common Mistakes
1. Not Disabling Introspection in Production
Without introspection: false, anyone can run __schema queries to see your entire API — types, fields, arguments, and even deprecated fields.
2. Ignoring Query Depth Limits
Without depth limits, a client can craft query { device { threats { device { threats { ... } } } } } — an infinite loop that crashes your server.
3. Using Synchronous PubSub in Production
graphql-subscriptions’s PubSub uses an in-memory event emitter — it doesn’t scale across multiple server instances. Use RedisPubSub for production.
4. Not Batching Subscription Events
Publishing individual events for each threat creates overhead. Batch events and use withFilter for client-side filtering.
5. Overusing Subscriptions
Not everything needs real-time updates. Use subscriptions only for live events (threat alerts, chat messages). For periodic data refreshes, use polling queries with pollInterval.
Practice Questions
- What is the difference between queries and subscriptions?
- How does schema federation help microservice architectures?
- Why disable introspection in production?
- What is query depth limiting and why is it important?
- How does RedisPubSub differ from in-memory PubSub?
Answers:
- Queries are request-response (client pulls). Subscriptions are push-based — the server sends data when events occur.
- Federation lets each microservice own part of the schema. The gateway composes them into a unified API, so clients see one schema.
- Introspection exposes the entire schema to anyone. In production, this reveals internal APIs and gives attackers information about your data model.
- Depth limiting restricts how nested a query can be. It prevents malicious or accidental deeply nested queries from overloading the server.
- RedisPubSub uses Redis as a message broker, enabling subscriptions to work across multiple server instances. In-memory PubSub only works within a single process.
Challenge: Design a real-time threat monitoring system for Durga Antivirus Pro using GraphQL subscriptions. The system should: (1) allow clients to subscribe to threats for specific devices, (2) notify all admins of critical threats, (3) batch threat events every 5 seconds before publishing, and (4) include device name and user email in the notification.
FAQ
Try It Yourself
// Simple subscription server
const { ApolloServer, gql, PubSub } = require('apollo-server');
const pubsub = new PubSub();
const typeDefs = gql`
type Threat { id: ID! name: String! }
type Query { threats: [Threat!]! }
type Mutation { addThreat(name: String!): Threat! }
type Subscription { threatAdded: Threat! }
`;
const resolvers = {
Query: { threats: () => [] },
Mutation: {
addThreat: (_, { name }) => {
const threat = { id: String(Date.now()), name };
pubsub.publish('THREAT_ADDED', { threatAdded: threat });
return threat;
},
},
Subscription: {
threatAdded: { subscribe: () => pubsub.asyncIterator('THREAT_ADDED') },
},
};
new ApolloServer({ typeDefs, resolvers }).listen(4000);What’s Next
| Topic | Description |
|---|---|
| GraphQL Introduction | Review core GraphQL concepts |
| Mutations Guide | Create, update, and delete patterns |
| RESTful APIs | Compare architectural patterns |
| SOAP & Web Services | Enterprise API architecture comparison |
What’s Next
Congratulations on completing this Graphql Architecture tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro