System Design 9 min read

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.

MR

Moshiour Rahman

Advertisement

API Design Mastery Series

This is Part 6 of our comprehensive API Design series.

PartTopicLevel
1HTTP & REST FundamentalsBeginner
2Security & AuthenticationBeginner
3Rate Limiting & PaginationIntermediate
4Versioning & IdempotencyIntermediate
5Caching StrategiesIntermediate
6GraphQL & gRPCIntermediate
7Resilience & ObservabilityAdvanced
8Production MasteryAdvanced

REST vs GraphQL vs gRPC

Protocol Comparison: REST vs GraphQL vs gRPC

AspectRESTGraphQLgRPC
Data FetchingFixed endpointsClient specifiesStrongly typed
OverfetchingCommonSolvedMinimal
UnderfetchingMultiple requestsSingle queryStreaming
VersioningURL/headerDeprecationPackage versions
CachingHTTP nativeCustomCustom
Real-timePolling/WebSocketSubscriptionsBi-di streaming
Browser SupportNativeNativeNeeds proxy
Best ForPublic APIsMobile apps, complex UIsMicroservices

When to Use What

ConsumerData NeedsPerformanceChoose
External developersSimple CRUDStandardREST
Same company frontendMany views of same dataStandardGraphQL
Internal microservicesHigh throughputCriticalgRPC
Mobile appsBandwidth constrainedImportantGraphQL
Browser clientsReal-time updatesStandardREST + 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

PatternUse CaseExample
UnaryRequest-responseGetUser, CreateUser
Server streamingLarge datasets, real-time eventsGetUserActivity
Client streamingBatch uploadsBatchCreateUsers
BidirectionalReal-time communicationUserPresence

Interview Question: “When would you choose gRPC over REST?”

Strong Answer: “gRPC excels in specific scenarios:

  1. Microservices communication: Binary protocol (protobuf) is 10x smaller and faster than JSON. Strongly typed contracts catch errors at compile time.

  2. High throughput: HTTP/2 multiplexing allows many concurrent requests over single connection. No head-of-line blocking.

  3. Streaming: Native support for server, client, and bidirectional streaming. Essential for real-time features.

  4. 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:

ThreatProblemMitigation
Query depth attackDeeply nested queries exhaust resourcesDepth limiting (max 10-15)
Query complexityWide queries with many fieldsComplexity scoring + limits
Batch attacksMany operations in one requestOperation count limits
Introspection exposureSchema reveals business logicDisable in production
Field-level authorizationNot all users should see all fieldsResolver-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

MistakeProblemFix
No DataLoaderN+1 queries, slow responsesBatch related queries
Over-fetching in resolversFetching data not requestedOnly resolve requested fields
No query complexity limitsDoS via expensive queriesImplement cost analysis
Introspection in prodSchema exposureDisable for production
No error maskingStack traces leak to clientCustom error formatting
Mutations return voidClient needs to re-fetchReturn affected object

gRPC Error Handling

gRPC uses status codes different from HTTP:

gRPC CodeHTTP EquivalentWhen to Use
OK (0)200Success
INVALID_ARGUMENT (3)400Bad request data
NOT_FOUND (5)404Resource doesn’t exist
ALREADY_EXISTS (6)409Duplicate creation
PERMISSION_DENIED (7)403Authenticated but unauthorized
UNAUTHENTICATED (16)401Missing/invalid auth
RESOURCE_EXHAUSTED (8)429Rate limited
INTERNAL (13)500Server error
UNAVAILABLE (14)503Service unavailable
DEADLINE_EXCEEDED (4)504Timeout
// 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)
  }
};
PhaseEffortRiskPerformance
ProxyLowLowSame as REST
BatchingMediumLowBetter (fewer requests)
Direct DBHighMediumBest (optimal queries)

GraphQL & gRPC Quick Reference

API Protocol Selection Guide


What’s Next?

Now that you understand modern API protocols, Part 7: Resilience & Observability covers circuit breakers, retries, and monitoring patterns.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

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

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.