SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Patterns — Designing GraphQL APIs

# Contents

  • Connection SDL (Relay style)
  • Mutation with union result
  • DataLoader implementation
  • Depth and complexity limits
  • Deprecation flow
  • Gotchas

# Connection SDL (Relay style)

graphql
type Query {
  orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!
}

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
  totalCount: Int          # nullable: expensive, resolve only when asked
}

type OrderEdge {
  node: Order!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

Clamp first to a max of 100 in the resolver (same reasoning as REST page limits); reject negative values with a validation error.

# Mutation with union result

graphql
input CreateOrderInput {
  customerId: ID!
  items: [OrderItemInput!]!
}

union CreateOrderResult = CreateOrderSuccess | ValidationError | OutOfStockError

type CreateOrderSuccess { order: Order! }
type ValidationError { fields: [FieldError!]! }
type OutOfStockError { itemIds: [ID!]!, message: String! }

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderResult!
}

Client handles each case via ... on fragments; the top-level errors array stays reserved for genuine faults (auth failure, internal error).

# DataLoader implementation

python
# Python (aiodataloader); same shape in JS's dataloader package.
from aiodataloader import DataLoader

class CustomerLoader(DataLoader):
    async def batch_load_fn(self, ids):
        rows = await db.fetch(
            "SELECT * FROM customers WHERE id = ANY($1)", ids)
        by_id = {r["id"]: r for r in rows}
        # Must return results in the SAME ORDER as ids, None for misses.
        return [by_id.get(i) for i in ids]

# Create ONE loader per request (in context), never a module-level singleton —
# its cache would leak data across users.
async def resolve_customer(order, info):
    return await info.context["customer_loader"].load(order["customer_id"])

# Depth and complexity limits

javascript
// graphql-depth-limit + graphql-query-complexity (npm)
validationRules: [
  depthLimit(10),                       // pathological nesting stops here
  createComplexityRule({
    maximumComplexity: 1000,            // ~1 point per field
    listFactor: 10,                     // lists multiply child cost
    onComplete: (c) => log.info({ complexity: c }),
  }),
]

Return the budget in the rejection message so clients can adapt: "Query complexity 2140 exceeds maximum 1000".

# Deprecation flow

graphql
type Order {
  total: Money!
  amount: Int @deprecated(reason: "Use total; amount is cents-only and removed after 2027-01-01.")
}
  1. Add the replacement field. 2. Deprecate with a reason that names the replacement and a date. 3. Monitor field usage (most gateways report it).
  2. Remove only when usage is zero or the date passes.

# Gotchas

  • Non-null cascade: an error in Order.customer: Customer! nulls the entire order — with Customer (nullable) only the field nulls. This is why rule 2 defaults to nullable.
  • DataLoader order contract: batch_load_fn must return exactly len(ids) results in input order; returning a dict or short list corrupts unrelated resolvers silently.
  • Enums over booleans: status: OrderStatus beats isActive/isArchived boolean pairs that can contradict each other.
  • Introspection in production: disable for public APIs unless the API is deliberately open; it enumerates your entire attack surface.
  • totalCount on large tables is a full COUNT(*) per query — make it nullable and resolve lazily, or return an estimate and say so.
  • Cursor stability: cursors must encode the ORDER BY key, not row position, or pagination skips/duplicates under concurrent writes.