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

Graphql Relay

skill-maheshawasare-claude-skills-pro-graphql-relay · by MaheshAwasare

Build a GraphQL API following Relay specifications — schema design, global IDs, connections (cursor pagination), DataLoader for N+1, persisted queries, auth at field level, and the operational discipline that prevents schema drift. Use when designing a new GraphQL API, not when adding fields to an existing one.

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

Install

$ agentstack add skill-maheshawasare-claude-skills-pro-graphql-relay

✓ 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 No
  • 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-maheshawasare-claude-skills-pro-graphql-relay)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo 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 Relay? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

GraphQL with Relay Conventions

GraphQL is two products: a query language (great) and a server pattern (foot-guns everywhere). Relay's spec is the cleaned-up server pattern. Adopt it even if your client isn't Relay — it eliminates 80% of GraphQL pathologies.

When to use

  • New GraphQL API.
  • You need fine-grained client-controlled data fetching (multi-team mobile/web).
  • Federation across services with Apollo Federation v2.

When NOT to use

  • Simple CRUD app — REST or tRPC is faster to build.
  • Hyper-low-latency public API — query parsing + N+1 risk overhead.
  • Internal admin tool — overkill.

Core Relay conventions

  1. Global IDs. Every node has a globally unique opaque ID, base64-encoded {type}:{rowid}.
  2. Node interface. Any object with a global ID implements Node.
  3. Connections. Lists are Connection with edges, pageInfo, cursor pagination.
  4. Mutations as input/payload pairs. Input! argument, Payload return with clientMutationId.

These four conventions remove ambiguity and enable client-side caching that actually works.

Schema example

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!                                          # global ID
  email: String!
  projects(first: Int, after: String): ProjectConnection!
}

type Project implements Node {
  id: ID!
  name: String!
  owner: User!
}

type ProjectConnection {
  edges: [ProjectEdge!]!
  pageInfo: PageInfo!
}

type ProjectEdge {
  node: Project!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  node(id: ID!): Node                              # generic fetch by global ID
  viewer: User                                     # authenticated user
}

input CreateProjectInput {
  name: String!
  clientMutationId: String                         # for optimistic updates
}

type CreateProjectPayload {
  project: Project!
  clientMutationId: String
}

type Mutation {
  createProject(input: CreateProjectInput!): CreateProjectPayload!
}

Global IDs

import { Buffer } from "node:buffer";

export function toGlobalId(type: string, id: string): string {
  return Buffer.from(`${type}:${id}`).toString("base64url");
}

export function fromGlobalId(globalId: string): { type: string; id: string } {
  const [type, id] = Buffer.from(globalId, "base64url").toString().split(":");
  return { type, id };
}

Why opaque: clients can't accidentally couple to the underlying ID format (UUID vs autoincrement vs Stripe ID). You can refactor freely.

DataLoader (the only solution to N+1)

Without DataLoader, asking for 100 projects each with .owner runs 101 queries. With DataLoader, batched into one.

import DataLoader from "dataloader";

export function makeUserLoader(db) {
  return new DataLoader(async (ids) => {
    const rows = await db.user.findMany({ where: { id: { in: [...ids] } } });
    const byId = new Map(rows.map((r) => [r.id, r]));
    return ids.map((id) => byId.get(id) ?? null);
  });
}

// In context (per-request)
export async function context({ req }) {
  return {
    userId: await verifyAuth(req),
    loaders: {
      user: makeUserLoader(db),
      project: makeProjectLoader(db),
    },
  };
}

// In resolver
const Project = {
  owner: (parent, _args, { loaders }) => loaders.user.load(parent.ownerId),
};

Critical: loaders are per-request. Caching across requests = stale data leaks. New context, new loaders.

Connection pagination (cursor, not offset)

projects(first: 20, after: "Y3Vyc29yMTIz")
// Resolver
async function projects(_parent, { first = 20, after }, ctx) {
  const cursor = after ? decodeCursor(after) : null;
  const rows = await db.project.findMany({
    where: cursor ? { createdAt: { lt: cursor.createdAt } } : {},
    orderBy: { createdAt: "desc" },
    take: first + 1,                     // fetch one extra to detect hasNextPage
  });
  const hasNextPage = rows.length > first;
  const nodes = rows.slice(0, first);
  return {
    edges: nodes.map((n) => ({ node: n, cursor: encodeCursor({ createdAt: n.createdAt }) })),
    pageInfo: {
      hasNextPage,
      hasPreviousPage: !!after,
      startCursor: nodes[0] && encodeCursor({ createdAt: nodes[0].createdAt }),
      endCursor: nodes.at(-1) && encodeCursor({ createdAt: nodes.at(-1)!.createdAt }),
    },
  };
}

Offset pagination breaks under writes (rows shift). Cursor pagination is stable.

Persisted queries (security + perf)

In production, accept only pre-registered queries by hash. Clients send { extensions: { persistedQuery: { sha256Hash: "..." } } } instead of the raw GraphQL.

Benefits:

  • Smaller payloads (bytes, not strings).
  • Attack surface reduction — can't run arbitrary queries.
  • CDN-cacheable — GET request with hash in URL.

Apollo Server has built-in support; tooling auto-generates the hash registry from your client codebase.

Auth at the field level

const Project = {
  // Public field
  name: (parent) => parent.name,
  // Owner-only field
  apiKey: (parent, _args, ctx) => {
    if (parent.ownerId !== ctx.userId) throw new Error("forbidden");
    return parent.apiKey;
  },
};

For more than 3 such checks, use a directive:

type Project {
  name: String!
  apiKey: String! @auth(role: OWNER)
}

Query complexity / depth limits

import { createComplexityLimitRule } from "graphql-validation-complexity";
import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  schema,
  validationRules: [
    depthLimit(10),
    createComplexityLimitRule(1000, { onCost: (cost) => log.info(cost) }),
  ],
});

Without these, a malicious client can submit { user { friends { friends { friends { ... } } } } } and DoS your DB.

Anti-patterns

  • N+1 without DataLoader — guaranteed perf bug. Loader-everything from day 1.
  • Returning database row IDs as id — couples client to schema; can't refactor. Use global IDs.
  • Offset pagination (limit/offset) — breaks under writes, doesn't scale past ~10k rows.
  • No depth/complexity limits — DoS waiting to happen.
  • One huge resolver file — split per type. User.ts, Project.ts, etc.
  • Mutations that return only Boolean — clients can't update local cache. Always return the affected entity.
  • Loader cache shared across requests — stale-data leak between users.
  • Persisting queries skipped for "later" — almost never gets done. Bake into client codegen from day 1.
  • No viewer query — every client has to remember its own user ID. viewer is the canonical "who am I?" entry point.
  • Public-field auth in resolver code — easy to forget. Use directives or shadow types.
  • Federation v1 — deprecated. v2 only for new projects.

Verify it worked

  • [ ] Fetching 100 projects with their owners hits DB ≤ 2 times (one for projects, one batched for users).
  • [ ] Asking for node(id: "") returns the correct typed entity.
  • [ ] Pagination is cursor-based; mid-write list is consistent (no skipped or duplicate rows).
  • [ ] Production accepts only persisted queries; raw GraphQL POST is rejected (or feature-flagged off).
  • [ ] Query depth > 10 is rejected with a useful error.
  • [ ] Per-request DataLoader; user A's request never sees cached entities from user B's.
  • [ ] viewer query returns the authenticated user; null when unauthenticated.
  • [ ] Mutations return the affected entity, not just Boolean.
  • [ ] Field-level auth: querying apiKey of someone else's project errors, doesn't leak.
  • [ ] Schema is in version control; CI runs schema diff and fails on breaking changes.

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.