API Design Part 6: GraphQL & gRPC
Master modern API protocols beyond REST. Learn when to use GraphQL for flexible queries, gRPC for high-performance microservices, and how to implement both in production.
Moshiour Rahman
Advertisement
API Design Mastery Series
This is Part 6 of our comprehensive API Design series.
| Part | Topic | Level |
|---|---|---|
| 1 | HTTP & REST Fundamentals | Beginner |
| 2 | Security & Authentication | Beginner |
| 3 | Rate Limiting & Pagination | Intermediate |
| 4 | Versioning & Idempotency | Intermediate |
| 5 | Caching Strategies | Intermediate |
| 6 | GraphQL & gRPC | Intermediate |
| 7 | Resilience & Observability | Advanced |
| 8 | Production Mastery | Advanced |
REST vs GraphQL vs gRPC
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Data Fetching | Fixed endpoints | Client specifies | Strongly typed |
| Overfetching | Common | Solved | Minimal |
| Underfetching | Multiple requests | Single query | Streaming |
| Versioning | URL/header | Deprecation | Package versions |
| Caching | HTTP native | Custom | Custom |
| Real-time | Polling/WebSocket | Subscriptions | Bi-di streaming |
| Browser Support | Native | Native | Needs proxy |
| Best For | Public APIs | Mobile apps, complex UIs | Microservices |
When to Use What
| Consumer | Data Needs | Performance | Choose |
|---|---|---|---|
| External developers | Simple CRUD | Standard | REST |
| Same company frontend | Many views of same data | Standard | GraphQL |
| Internal microservices | High throughput | Critical | gRPC |
| Mobile apps | Bandwidth constrained | Important | GraphQL |
| Browser clients | Real-time updates | Standard | REST + WebSocket or GraphQL |
GraphQL Implementation Patterns
// graphql-server.ts - Production GraphQL setup
import { createSchema, createYoga } from 'graphql-yoga';
import { createContext, Context } from './context';
// DataLoader for N+1 prevention
import DataLoader from 'dataloader';
interface User {
id: string;
email: string;
name: string;
organizationId: string;
}
interface Organization {
id: string;
name: string;
}
// Batch loading functions
async function batchLoadUsers(ids: readonly string[]): Promise<User[]> {
const users = await db.user.findMany({
where: { id: { in: [...ids] } }
});
// Must return in same order as input
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id)!);
}
async function batchLoadOrganizations(ids: readonly string[]): Promise<Organization[]> {
const orgs = await db.organization.findMany({
where: { id: { in: [...ids] } }
});
const orgMap = new Map(orgs.map(o => [o.id, o]));
return ids.map(id => orgMap.get(id)!);
}
// Create DataLoaders per request
function createLoaders() {
return {
user: new DataLoader(batchLoadUsers),
organization: new DataLoader(batchLoadOrganizations)
};
}
const typeDefs = /* GraphQL */ `
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
me: User
}
type Mutation {
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type User {
id: ID!
email: String!
name: String!
organization: Organization!
posts(first: Int, after: String): PostConnection!
createdAt: DateTime!
}
type Organization {
id: ID!
name: String!
members(first: Int): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input UpdateUserInput {
name: String
email: String
}
scalar DateTime
`;
const resolvers = {
Query: {
user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
return ctx.loaders.user.load(id);
},
me: async (_: unknown, __: unknown, ctx: Context) => {
if (!ctx.user) throw new Error('Not authenticated');
return ctx.loaders.user.load(ctx.user.id);
}
},
User: {
// Resolved via DataLoader to prevent N+1
organization: async (user: User, _: unknown, ctx: Context) => {
return ctx.loaders.organization.load(user.organizationId);
}
},
Mutation: {
updateUser: async (
_: unknown,
{ id, input }: { id: string; input: { name?: string; email?: string } },
ctx: Context
) => {
// Authorization check
if (ctx.user?.id !== id && !ctx.user?.isAdmin) {
throw new Error('Forbidden');
}
const user = await db.user.update({
where: { id },
data: input
});
// Clear cache
ctx.loaders.user.clear(id);
return user;
}
}
};
const schema = createSchema({ typeDefs, resolvers });
export const yoga = createYoga({
schema,
context: async ({ request }) => {
const user = await authenticateRequest(request);
return {
user,
loaders: createLoaders()
};
},
// Security: Limit query complexity
plugins: [
useQueryComplexity({
maximumComplexity: 100,
variables: {},
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
}
}),
useDepthLimit({ maxDepth: 10 })
]
});
gRPC for Internal Services
// user_service.proto - gRPC service definition
syntax = "proto3";
package user.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
// Server streaming - get user activity
rpc GetUserActivity(GetUserActivityRequest) returns (stream ActivityEvent);
// Client streaming - batch create users
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateResponse);
// Bidirectional streaming - real-time presence
rpc UserPresence(stream PresenceUpdate) returns (stream PresenceStatus);
}
message User {
string id = 1;
string email = 2;
string name = 3;
string organization_id = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string email = 1;
string name = 2;
string organization_id = 3;
}
message UpdateUserRequest {
string id = 1;
optional string email = 2;
optional string name = 3;
}
message DeleteUserRequest {
string id = 1;
}
message GetUserActivityRequest {
string user_id = 1;
google.protobuf.Timestamp since = 2;
}
message ActivityEvent {
string id = 1;
string type = 2;
string description = 3;
google.protobuf.Timestamp timestamp = 4;
}
message BatchCreateResponse {
int32 created_count = 1;
int32 failed_count = 2;
repeated string failed_emails = 3;
}
message PresenceUpdate {
string user_id = 1;
PresenceState state = 2;
}
message PresenceStatus {
string user_id = 1;
PresenceState state = 2;
google.protobuf.Timestamp last_seen = 3;
}
enum PresenceState {
PRESENCE_STATE_UNSPECIFIED = 0;
PRESENCE_STATE_ONLINE = 1;
PRESENCE_STATE_AWAY = 2;
PRESENCE_STATE_OFFLINE = 3;
}
gRPC Communication Patterns
| Pattern | Use Case | Example |
|---|---|---|
| Unary | Request-response | GetUser, CreateUser |
| Server streaming | Large datasets, real-time events | GetUserActivity |
| Client streaming | Batch uploads | BatchCreateUsers |
| Bidirectional | Real-time communication | UserPresence |
Interview Question: “When would you choose gRPC over REST?”
Strong Answer: “gRPC excels in specific scenarios:
-
Microservices communication: Binary protocol (protobuf) is 10x smaller and faster than JSON. Strongly typed contracts catch errors at compile time.
-
High throughput: HTTP/2 multiplexing allows many concurrent requests over single connection. No head-of-line blocking.
-
Streaming: Native support for server, client, and bidirectional streaming. Essential for real-time features.
-
Polyglot systems: Proto files generate clients/servers in 12+ languages. Ensures consistency across services.
However, REST wins for public APIs (browser support, ubiquity), simple CRUD (less tooling overhead), and when HTTP caching is important.”
GraphQL Security Considerations
GraphQL’s flexibility introduces unique security challenges:
| Threat | Problem | Mitigation |
|---|---|---|
| Query depth attack | Deeply nested queries exhaust resources | Depth limiting (max 10-15) |
| Query complexity | Wide queries with many fields | Complexity scoring + limits |
| Batch attacks | Many operations in one request | Operation count limits |
| Introspection exposure | Schema reveals business logic | Disable in production |
| Field-level authorization | Not all users should see all fields | Resolver-level auth checks |
// graphql-security.ts - Security middleware
import { createYoga } from 'graphql-yoga';
import { useDepthLimit } from '@escape.tech/graphql-armor-depth-limit';
import { useCostLimit } from '@escape.tech/graphql-armor-cost-limit';
export const yoga = createYoga({
schema,
plugins: [
// Prevent deep nesting attacks
useDepthLimit({ n: 10 }),
// Prevent wide query attacks
useCostLimit({
maxCost: 1000,
objectCost: 1,
scalarCost: 0,
depthCostFactor: 2,
}),
// Disable introspection in production
process.env.NODE_ENV === 'production' && useDisableIntrospection(),
].filter(Boolean),
});
GraphQL Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| No DataLoader | N+1 queries, slow responses | Batch related queries |
| Over-fetching in resolvers | Fetching data not requested | Only resolve requested fields |
| No query complexity limits | DoS via expensive queries | Implement cost analysis |
| Introspection in prod | Schema exposure | Disable for production |
| No error masking | Stack traces leak to client | Custom error formatting |
| Mutations return void | Client needs to re-fetch | Return affected object |
gRPC Error Handling
gRPC uses status codes different from HTTP:
| gRPC Code | HTTP Equivalent | When to Use |
|---|---|---|
OK (0) | 200 | Success |
INVALID_ARGUMENT (3) | 400 | Bad request data |
NOT_FOUND (5) | 404 | Resource doesn’t exist |
ALREADY_EXISTS (6) | 409 | Duplicate creation |
PERMISSION_DENIED (7) | 403 | Authenticated but unauthorized |
UNAUTHENTICATED (16) | 401 | Missing/invalid auth |
RESOURCE_EXHAUSTED (8) | 429 | Rate limited |
INTERNAL (13) | 500 | Server error |
UNAVAILABLE (14) | 503 | Service unavailable |
DEADLINE_EXCEEDED (4) | 504 | Timeout |
// error-details.proto - Rich error information
import "google/rpc/error_details.proto";
// Return detailed errors
rpc CreateUser(CreateUserRequest) returns (User) {
// On validation error, return:
// status: INVALID_ARGUMENT
// details: [BadRequest with field_violations]
}
Migration from REST to GraphQL
// Step 1: GraphQL layer wraps existing REST endpoints
const resolvers = {
Query: {
user: async (_, { id }) => {
// Initially, just proxy to REST
const res = await fetch(`${REST_API}/users/${id}`);
return res.json();
}
}
};
// Step 2: Add DataLoader for batching
const userLoader = new DataLoader(async (ids) => {
const res = await fetch(`${REST_API}/users?ids=${ids.join(',')}`);
return res.json();
});
// Step 3: Direct database access (final state)
const resolvers = {
Query: {
user: (_, { id }, ctx) => ctx.loaders.user.load(id)
}
};
| Phase | Effort | Risk | Performance |
|---|---|---|---|
| Proxy | Low | Low | Same as REST |
| Batching | Medium | Low | Better (fewer requests) |
| Direct DB | High | Medium | Best (optimal queries) |
GraphQL & gRPC Quick Reference

What’s Next?
Now that you understand modern API protocols, Part 7: Resilience & Observability covers circuit breakers, retries, and monitoring patterns.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
API Design Mastery: Complete 8-Part Series
Master API design from HTTP fundamentals to production systems. 8-part comprehensive guide covering REST, security, caching, GraphQL, gRPC, resilience, and interview preparation.
System DesignAPI Design Part 5: Caching Strategies
Master multi-layer caching architecture, HTTP cache headers, ETags, and cache invalidation patterns. Build fast, scalable APIs with proper caching.
System DesignAPI Design Part 1: HTTP & REST Fundamentals
Master HTTP methods, status codes, and REST maturity model. The foundation every API developer needs - from GET/POST basics to idempotency and proper status code selection.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.