AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Graphql Grpc Api Patterns

skill-mickeyyaya-refactoring-skills-graphql-grpc-api-patterns · by mickeyyaya

Use when reviewing or designing GraphQL and gRPC APIs — covers schema design and evolution, N+1/DataLoader batching, subscriptions, Protobuf field numbering, gRPC streaming modes, breaking-change detection, anti-patterns, and code review signals across TypeScript, Go, and Java

No reviews yet
0 installs
12 views
0.0% view→install

Install

$ agentstack add skill-mickeyyaya-refactoring-skills-graphql-grpc-api-patterns

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mickeyyaya-refactoring-skills-graphql-grpc-api-patterns)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
5mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Graphql Grpc Api Patterns? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

GraphQL and gRPC API Patterns for Code Review

Overview

GraphQL and gRPC each solve different API design problems but share a common failure mode: schema or contract changes that break callers without warning. GraphQL's flexibility makes it easy to over-fetch, under-batch, and leak implementation details. gRPC's strict Protobuf contracts are powerful but unforgiving when field numbers or types are changed carelessly.

When to use: Reviewing GraphQL resolvers, schema migrations, Protobuf .proto files, gRPC service definitions, or any code that consumes or exposes these APIs; evaluating batching strategies, subscription implementations, or streaming service designs.

Quick Reference

| Pattern | Core Idea | Primary Red Flag | |---------|-----------|-----------------| | GraphQL Schema Design | Types, fields, nullability as API contract | Nullable fields everywhere; exposing DB columns directly | | Schema Evolution (GraphQL) | Deprecate over version; never remove live fields | Removing fields without @deprecated; adding breaking nullability changes | | N+1 / DataLoader | Batch per-field DB lookups into one query | Per-resolver DB call inside a list type; no DataLoader | | GraphQL Subscriptions | Real-time push via WebSocket/SSE | Subscriptions without auth checks; no connection cleanup | | Protobuf Design | Field numbering and naming are the wire contract | Reusing field numbers; renaming fields without alias | | gRPC Streaming | Four modes: unary, server, client, bidirectional | Streaming without deadline/cancel propagation | | Schema Evolution (gRPC) | Backward-compatible field additions only | Removing/renumbering fields; changing field types | | Breaking Change Detection | Automate with buf or protoc-gen-compat | Manual review only; no CI check on .proto changes |


Patterns in Detail

1. GraphQL Schema Design and Nullability

Red Flags:

  • Every field is nullable — callers must null-check every level, and errors are invisible
  • Schema mirrors DB tables — leaks storage concerns into the API contract
  • Input types reused as output types — different validation needs collide
  • ID fields typed as String or Int inconsistently — prevents global object identification

TypeScript (schema-first with graphql-tag):

// BEFORE — nullable everywhere; DB table exposed directly
type User {
  id: String
  name: String
  email: String
  internal_account_id: String  # storage detail leaked
}

// AFTER — nullable only where absence is meaningful; IDs use scalar ID
type User {
  id: ID!
  name: String!
  email: String!
  # internal_account_id removed — not a public concern
  createdAt: DateTime!
  avatarUrl: String          # nullable: user may not have set one
}

type Query {
  user(id: ID!): User        # nullable return: user may not exist
  me: User!                  # non-nullable: if authed, always present
}

TypeScript (code-first with type-graphql):

@ObjectType()
class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;  // non-null by default in type-graphql

  @Field({ nullable: true })
  avatarUrl?: string;
}

Java (DGS framework):

@DgsComponent
public class UserDataFetcher {
    @DgsQuery
    public User user(@InputArgument String id) {
        return userService.findById(id)
            .orElseThrow(() -> new DgsEntityNotFoundException("User not found: " + id));
    }
}

2. GraphQL Schema Evolution — Deprecation Over Versioning

Red Flags:

  • Field removed without @deprecated directive and a migration period
  • New required (non-null) argument added to existing field — breaks existing queries
  • schema.design changed by renaming a field — old queries fail silently
  • Breaking nullability change: field changed from String to String! without client audit

Schema deprecation — TypeScript:

// WRONG: just delete the field
type User {
  id: ID!
  name: String!
  # username removed — breaks all existing clients
}

// CORRECT: mark deprecated first; remove only after all callers migrate
type User {
  id: ID!
  name: String!
  username: String @deprecated(reason: "Use `name` instead. Removed after 2026-06-01.")
}

Adding arguments safely (TypeScript):

// WRONG — adding required arg breaks existing callers
users(limit: Int!, offset: Int!): [User!]!

// CORRECT — new arguments must have defaults so old queries still work
users(limit: Int = 20, offset: Int = 0): [User!]!

Evolut schema with interface extension — Go (gqlgen):

// schema.graphqls
extend type Query {
  # New field added — non-breaking; old queries unaffected
  usersByOrg(orgId: ID!, limit: Int = 20): [User!]!
}

Cross-reference: review-api-contract — API versioning strategies and backward compatibility rules.


3. GraphQL N+1 Problem and DataLoader Batching

Red Flags:

  • A resolver for a field on a list type makes a database call per item — N+1 queries
  • DataLoader created inside the resolver function (not per-request) — defeats batching
  • No DataLoader for any has-many or belongs-to relationship
  • Promise.all used correctly but still bypasses batching — parallel but not coalesced

TypeScript — N+1 before/after:

// BEFORE — N+1: 1 query for posts + N queries for each author
const PostResolver = {
  author: async (post: Post) => {
    return db.users.findById(post.authorId);  // called once per post in list
  },
};

// AFTER — DataLoader coalesces all author IDs into a single batch query
import DataLoader from 'dataloader';

function createLoaders() {
  return {
    userLoader: new DataLoader(async (ids) => {
      const users = await db.users.findByIds([...ids]);
      const userMap = new Map(users.map(u => [u.id, u]));
      return ids.map(id => userMap.get(id) ?? new Error(`User not found: ${id}`));
    }),
  };
}

// Loader attached to request context — one instance per request, not per field
const PostResolver = {
  author: (post: Post, _args: unknown, ctx: Context) => {
    return ctx.loaders.userLoader.load(post.authorId);
  },
};

Go (gqlgen + dataloaden):

// generated UserLoader batches by []string IDs
func (r *queryResolver) Posts(ctx context.Context) ([]*model.Post, error) {
    posts, err := r.db.AllPosts(ctx)
    if err != nil { return nil, err }
    return posts, nil
}

// Per-field resolver uses the loader — no direct DB call
func (r *postResolver) Author(ctx context.Context, post *model.Post) (*model.User, error) {
    return getLoaders(ctx).UserLoader.Load(post.AuthorID)
}

Java (Spring GraphQL + @BatchMapping):

@BatchMapping(typeName = "Post", field = "author")
public Flux authors(List posts) {
    List ids = posts.stream().map(Post::getAuthorId).toList();
    return userService.findAllByIds(ids);  // single DB call for all posts
}

4. GraphQL Subscriptions and Real-Time Patterns

Red Flags:

  • Subscription resolvers without authentication checks — open WebSocket endpoints
  • No connection cleanup (unsubscribe / complete) — memory leaks under load
  • Publishing to all subscribers from a single resolver — fan-out bottleneck
  • Subscriptions sharing mutable state across connections — race conditions
  • Missing heartbeat/keep-alive — stale connections accumulate

TypeScript (Apollo Server + graphql-ws):

// schema
type Subscription {
  messageAdded(channelId: ID!): Message!
}

// resolver — subscription with auth guard and cleanup
const resolvers = {
  Subscription: {
    messageAdded: {
      subscribe: async function* (_, { channelId }, ctx) {
        if (!ctx.userId) throw new GraphQLError('Unauthorized', {
          extensions: { code: 'UNAUTHORIZED' },
        });
        const channel = await validateChannelAccess(ctx.userId, channelId);
        const iter = pubsub.asyncIterator(`MESSAGE_ADDED:${channel.id}`);
        try {
          for await (const payload of iter) {
            yield payload;
          }
        } finally {
          // cleanup runs when client disconnects
          iter.return?.();
        }
      },
    },
  },
};

Go (gqlgen subscriptions):

func (r *subscriptionResolver) MessageAdded(ctx context.Context, channelID string) (
): Promise {
  const cursor = db.orders.cursor({ userId: call.request.userId });
  try {
    for await (const order of cursor) {
      if (call.cancelled) break;  // respect client cancel
      call.write(orderToProto(order));
    }
    call.end();
  } catch (err) {
    call.destroy(err as Error);
  }
}

Go — bidirectional streaming with context propagation:

// proto: rpc Chat (stream ChatMessage) returns (stream ChatMessage);

func (s *ChatServer) Chat(stream pb.Chat_ChatServer) error {
    ctx := stream.Context()
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil  // client signalled half-close
        }
        if err != nil {
            return status.Errorf(codes.Internal, "recv: %v", err)
        }
        select {
        case  uploadMetrics(StreamObserver responseObserver) {
    List buffer = new ArrayList<>();
    return new StreamObserver<>() {
        @Override public void onNext(MetricPoint point) { buffer.add(point); }
        @Override public void onError(Throwable t) {
            log.error("Upload stream error", t);
            responseObserver.onError(t);
        }
        @Override public void onCompleted() {
            SummaryResponse summary = metricsService.summarize(buffer);
            responseObserver.onNext(summary);
            responseObserver.onCompleted();
        }
    };
}

7. Schema Evolution and Breaking Change Detection

Red Flags:

  • No buf or protoc-gen-compat check in CI — breaking changes merged silently
  • GraphQL schema changes deployed without graphql-inspector diff in PR
  • Field type changed (e.g., int32 to int64) — wire incompatible even if value fits
  • New non-null field added to input type without default — breaks existing callers
  • oneof field added to existing message without backward-compat analysis

buf.yaml — CI breaking change detection:

version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

CI workflow snippet (GitHub Actions):

- name: Check Protobuf breaking changes
  run: |
    buf breaking --against '.git#branch=main'

GraphQL Inspector in CI — TypeScript project:

# Compare current schema against main branch schema
npx graphql-inspector diff \
  'git:origin/main:schema.graphql' \
  './schema.graphql' \
  --onUsage breaking

Go — safe proto message evolution:

// BEFORE (v1)
message SearchRequest {
  string query = 1;
}

// AFTER (v2) — backward compatible additions
message SearchRequest {
  string query = 1;
  int32 max_results = 2;   // new optional field — old clients send 0 (proto default)
  repeated string filters = 3;  // new repeated — old clients send empty list
}

Cross-reference: error-handling-patterns — fail-fast validation at schema boundaries.


8. Anti-Patterns and Code Review Signals

| Anti-Pattern | Description | Fix | |-------------|-------------|-----| | Resolver N+1 | Per-item DB call inside list resolver | Introduce DataLoader; batch by parent IDs | | God Query | Single GraphQL query fetches entire object graph | Paginate lists; use @defer for non-critical fields | | Schema Versioning | /graphql/v2 endpoint instead of deprecating fields | Add @deprecated; version at field level, not endpoint | | Nullable Everything | All fields nullable to "be safe" | Make fields non-null unless absence is meaningful; document why nullable | | Field Number Reuse | Deleted proto field number reassigned | Always reserve deleted field numbers and names | | Missing Deadline | gRPC call or stream without deadline/timeout | Set WithDeadline or WithTimeout on every outgoing call | | Streaming Without Cancel | Server stream ignores context.Done() | Select on ctx.Done() in every stream loop iteration | | Open Subscription | Subscription endpoint lacks auth guard | Check credentials in subscribe function before yielding | | Input = Output Type | Same GraphQL type used for mutations and queries | Separate UserInput for writes; User for reads | | proto required Fields | Using required in proto2 locks schema forever | Use optional (proto3 default); validate in application layer |

God Query — TypeScript fix with @defer:

// BEFORE — fetches entire social graph in one round-trip
query UserPage($id: ID!) {
  user(id: $id) {
    id name email
    posts { id title body comments { id text author { name } } }
    followers { id name avatarUrl }
  }
}

// AFTER — critical data first; deferred sections stream in after
query UserPage($id: ID!) {
  user(id: $id) {
    id name email
    ... on User @defer(label: "posts") {
      posts { id title }
    }
    ... on User @defer(label: "followers") {
      followers { id name }
    }
  }
}

Missing deadline — Go:

// WRONG — no deadline; server stream runs forever on slow response
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewOrderServiceClient(conn)
stream, _ := client.ListOrders(context.Background(), req)

// CORRECT — deadline propagated; stream cancelled if server is slow
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stream, err := client.ListOrders(ctx, req)
if err != nil {
    return fmt.Errorf("ListOrders: %w", err)
}

Auth guard on subscription — Java (Spring GraphQL):

@SubscriptionMapping
public Flux messageAdded(@Argument String channelId,
                                   @AuthenticationPrincipal UserDetails user) {
    if (user == null) throw new AccessDeniedException("Authentication required");
    return messagingService.subscribe(channelId, user.getUsername())
        .doOnCancel(() -> messagingService.unsubscribe(channelId, user.getUsername()));
}

Cross-References

  • review-api-contract — API backward compatibility rules, versioning strategy, and contract testing
  • architectural-patterns — Event-driven architecture and pub/sub models that underpin subscriptions and streaming
  • error-handling-patterns — Fail-fast validation at schema and Protobuf message boundaries; error propagation in streaming contexts
  • security-patterns-code-review — Authentication on WebSocket/subscription endpoints; authorization in resolver context
  • observability-patterns — Tracing across gRPC boundaries; subscription connection metrics and DataLoader cache hit rates

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.