GraphQL Yoga is a spec-compliant GraphQL server built on fetch API — it runs on Node.js, Cloudflare Workers, Deno, Bun, and Next.js API routes with the same setup. createSchema({ typeDefs, resolvers }) builds the schema from SDL type definitions. Pothos provides a code-first builder alternative where types and resolvers are colocated. createYoga({ schema, context }) produces the server. Subscriptions use server-sent events — Yoga handles the SSE transport automatically via @graphql-yoga/subscription. GraphQLError with extensions.code returns structured error codes. graphql-shield adds resolver-level authorization rules. Persisted queries reduce request payload size. Claude Code generates GraphQL Yoga servers, Pothos schema builders, subscription resolvers, shield configurations, and the deployment wrappers for Cloudflare Workers and Next.js handlers.
CLAUDE.md for GraphQL Yoga
## GraphQL Yoga Stack
- Version: graphql-yoga >= 5.0, graphql >= 16.9
- Schema: createSchema({ typeDefs: gql`...`, resolvers }) — SDL first
- Code-first: Pothos with @pothos/core — builder.queryType, builder.mutationType
- Context: createYoga({ context: async ({ request }) => ({ db, currentUser }) })
- Subscriptions: Pub/Sub from @graphql-yoga/subscription — subscribe + resolve
- Auth: graphql-shield with rule() — compose allow/deny rules per field/type
- Errors: throw new GraphQLError("msg", { extensions: { code: "NOT_FOUND" } })
- Deploy: yoga.handleRequest for CF Workers, export { default: yoga } for Next.js
Schema-First Server
// src/schema.ts — SDL type definitions
import { createSchema } from "graphql-yoga"
import { gql } from "graphql-tag"
import { ordersResolvers } from "./resolvers/orders"
import { usersResolvers } from "./resolvers/users"
const typeDefs = gql`
scalar DateTime
scalar JSON
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type OrderItem {
productId: ID!
name: String!
quantity: Int!
priceCents: Int!
}
type Order {
id: ID!
status: OrderStatus!
items: [OrderItem!]!
totalCents: Int!
customer: User!
createdAt: DateTime!
updatedAt: DateTime!
}
type User {
id: ID!
email: String!
name: String!
orders(limit: Int, status: OrderStatus): [Order!]!
}
type OrdersConnection {
orders: [Order!]!
totalCount: Int!
hasNextPage: Boolean!
endCursor: String
}
input CreateOrderInput {
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
priceCents: Int!
name: String!
}
type Query {
order(id: ID!): Order
orders(
customerId: ID
status: OrderStatus
limit: Int
after: String
): OrdersConnection!
me: User
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
cancelOrder(id: ID!): Order!
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
}
type Subscription {
orderUpdated(orderId: ID!): Order!
newOrderForCustomer(customerId: ID!): Order!
}
`
export const schema = createSchema({
typeDefs,
resolvers: {
...ordersResolvers,
...usersResolvers,
},
})
Resolvers
// src/resolvers/orders.ts — resolver implementations
import { GraphQLError } from "graphql"
import type { GraphQLContext } from "../context"
export const ordersResolvers = {
Query: {
order: async (_: unknown, { id }: { id: string }, ctx: GraphQLContext) => {
const order = await ctx.db.orders.findById(id)
if (!order) {
throw new GraphQLError(`Order ${id} not found`, {
extensions: { code: "NOT_FOUND", orderId: id },
})
}
return order
},
orders: async (
_: unknown,
{
customerId,
status,
limit = 20,
after,
}: { customerId?: string; status?: string; limit?: number; after?: string },
ctx: GraphQLContext
) => {
const [orders, totalCount] = await Promise.all([
ctx.db.orders.list({ customerId, status, limit: limit + 1, after }),
ctx.db.orders.count({ customerId, status }),
])
const hasNextPage = orders.length > limit
const page = hasNextPage ? orders.slice(0, limit) : orders
const endCursor = page.length > 0 ? page[page.length - 1].id : null
return { orders: page, totalCount, hasNextPage, endCursor }
},
},
Mutation: {
createOrder: async (
_: unknown,
{ input }: { input: { items: OrderItemInput[] } },
ctx: GraphQLContext
) => {
if (!ctx.currentUser) {
throw new GraphQLError("Authentication required", {
extensions: { code: "UNAUTHENTICATED" },
})
}
const totalCents = input.items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
const order = await ctx.db.orders.create({
customerId: ctx.currentUser.id,
items: input.items,
totalCents,
status: "PENDING",
})
// Publish for subscriptions
ctx.pubSub.publish("ORDER_CREATED", { customerId: ctx.currentUser.id, order })
return order
},
cancelOrder: async (
_: unknown,
{ id }: { id: string },
ctx: GraphQLContext
) => {
const order = await ctx.db.orders.findById(id)
if (!order) throw new GraphQLError("Order not found", { extensions: { code: "NOT_FOUND" } })
if (order.customerId !== ctx.currentUser?.id) {
throw new GraphQLError("Forbidden", { extensions: { code: "FORBIDDEN" } })
}
if (!["PENDING", "PROCESSING"].includes(order.status)) {
throw new GraphQLError("Cannot cancel order in current status", {
extensions: { code: "BAD_USER_INPUT", currentStatus: order.status },
})
}
const updated = await ctx.db.orders.update(id, { status: "CANCELLED" })
ctx.pubSub.publish("ORDER_UPDATED", { orderId: id, order: updated })
return updated
},
},
Subscription: {
orderUpdated: {
subscribe: (_: unknown, { orderId }: { orderId: string }, ctx: GraphQLContext) =>
ctx.pubSub.subscribe("ORDER_UPDATED", orderId),
resolve: (payload: { order: Order }) => payload.order,
},
newOrderForCustomer: {
subscribe: (_: unknown, { customerId }: { customerId: string }, ctx: GraphQLContext) =>
ctx.pubSub.subscribe("ORDER_CREATED", customerId),
resolve: (payload: { order: Order }) => payload.order,
},
},
Order: {
customer: async (order: { customerId: string }, _: unknown, ctx: GraphQLContext) =>
ctx.db.users.findById(order.customerId),
},
User: {
orders: async (
user: { id: string },
{ limit = 20, status }: { limit?: number; status?: string },
ctx: GraphQLContext
) => ctx.db.orders.list({ customerId: user.id, status, limit }),
},
}
Context and PubSub
// src/context.ts — context factory with PubSub
import { createPubSub } from "@graphql-yoga/subscription"
import { db } from "./db"
import { verifyToken } from "./auth"
type PubSubEvents = {
ORDER_UPDATED: [orderId: string, payload: { orderId: string; order: Order }]
ORDER_CREATED: [customerId: string, payload: { customerId: string; order: Order }]
}
export const pubSub = createPubSub<PubSubEvents>()
export interface GraphQLContext {
db: typeof db
pubSub: typeof pubSub
currentUser: { id: string; email: string } | null
}
export async function createContext({
request,
}: {
request: Request
}): Promise<GraphQLContext> {
const auth = request.headers.get("Authorization") ?? ""
const currentUser = auth.startsWith("Bearer ")
? await verifyToken(auth.slice(7))
: null
return { db, pubSub, currentUser }
}
Yoga Server
// src/server.ts — Yoga server setup
import { createYoga } from "graphql-yoga"
import { usePersistedOperations } from "@graphql-yoga/plugin-persisted-operations"
import { schema } from "./schema"
import { createContext } from "./context"
import store from "./persisted-queries.json" // { "operationId": "query { ... }" }
// For Node.js / Bun
export const yoga = createYoga({
schema,
context: createContext,
plugins: [
// Persisted queries — only allow known operations in production
usePersistedOperations({
getPersistedOperation(key) {
return store[key as keyof typeof store] ?? null
},
allowArbitraryOperations: process.env.NODE_ENV !== "production",
}),
],
graphiql: process.env.NODE_ENV !== "production",
cors: {
origin: ["https://app.example.com"],
credentials: true,
},
})
// Node.js server
import { createServer } from "http"
const server = createServer(yoga)
server.listen(4000, () => {
console.log("GraphQL Yoga running at http://localhost:4000/graphql")
})
Next.js API Route
// app/api/graphql/route.ts — Next.js App Router
import { createYoga } from "graphql-yoga"
import { schema } from "@/graphql/schema"
import { createContext } from "@/graphql/context"
const { handleRequest } = createYoga({
schema,
context: createContext,
graphqlEndpoint: "/api/graphql",
fetchAPI: { Response, Request, ReadableStream },
})
export const GET = handleRequest
export const POST = handleRequest
Cloudflare Workers
// src/worker.ts — Cloudflare Workers deployment
import { createYoga } from "graphql-yoga"
import { schema } from "./schema"
const yoga = createYoga({ schema })
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return yoga.fetch(request, { env, ctx })
},
}
For the Apollo Server alternative when Apollo Studio tracing, federated supergraph architecture, or Apollo Client cache integration are needed beyond Yoga’s standalone server, see the GraphQL federation guide for Apollo federation setup. For the Pothos code-first GraphQL schema builder that eliminates SDL drift and colocates types with resolvers for large TypeScript-first schemas, the TypeScript API guide covers type-safe resolver patterns. The Claude Skills 360 bundle includes GraphQL Yoga skill sets covering schema design, subscriptions, and shield authorization. Start with the free tier to try GraphQL server generation.