--- id: backend-graphql-yoga-pothos title: GraphQL Yoga / Pothos — Modern GraphQL Server category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, graphql, yoga, pothos, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [GraphQL Yoga, Pothos, code-first GraphQL, Apollo Server alternative, Mercurius] --- # GraphQL Yoga / Pothos > Apollo Server 의 modern alternative. **Yoga (server) + Pothos (schema builder, type-safe)**. Edge runtime + 빠른 + 작은. Federation 지원. ## 📖 핵심 개념 - Yoga: Server (request handler). - Pothos: code-first schema builder (TS). - DataLoader: N+1 해결. - Federation: 마이크로서비스 결합. ## 💻 코드 패턴 ### Yoga + Pothos 시작 ```bash yarn add graphql graphql-yoga @pothos/core ``` ```ts import { createYoga } from 'graphql-yoga'; import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder<{ Context: { user: User | null; db: DB }; }>({}); builder.objectType('User', { fields: t => ({ id: t.exposeID('id'), email: t.exposeString('email'), name: t.exposeString('name', { nullable: true }), posts: t.field({ type: ['Post'], resolve: (user, _, ctx) => ctx.db.posts.findByUser(user.id), }), }), }); builder.queryType({ fields: t => ({ me: t.field({ type: 'User', nullable: true, resolve: (_, __, ctx) => ctx.user, }), }), }); const yoga = createYoga({ schema: builder.toSchema(), context: async ({ request }) => ({ user: await getUser(request), db, }), }); ``` ```ts // Server (Bun / Node) import { createServer } from 'node:http'; const server = createServer(yoga); server.listen(4000); ``` ### Type-safe input ```ts const CreatePostInput = builder.inputType('CreatePostInput', { fields: t => ({ title: t.string({ required: true }), body: t.string({ required: true }), tags: t.stringList(), }), }); builder.mutationType({ fields: t => ({ createPost: t.field({ type: 'Post', args: { input: t.arg({ type: CreatePostInput, required: true }), }, resolve: async (_, { input }, ctx) => { if (!ctx.user) throw new Error('UNAUTHORIZED'); return ctx.db.posts.create({ ...input, userId: ctx.user.id }); }, }), }), }); ``` → Schema + resolver type-safe. ### Pothos Prisma plugin ```ts import PrismaPlugin from '@pothos/plugin-prisma'; import type PrismaTypes from './generated/pothos-types'; const builder = new SchemaBuilder<{ PrismaTypes: PrismaTypes }>({ plugins: [PrismaPlugin], prisma: { client: prisma }, }); builder.prismaObject('User', { fields: t => ({ id: t.exposeID('id'), email: t.exposeString('email'), posts: t.relation('posts'), // N+1 자동 해결 }), }); builder.queryFields(t => ({ user: t.prismaField({ type: 'User', args: { id: t.arg.id({ required: true }) }, resolve: (query, _, { id }) => prisma.user.findUnique({ ...query, where: { id } }), }), })); ``` → Prisma + Pothos = N+1 자동. ### Drizzle plugin ```ts import { drizzlePlugin } from '@pothos/plugin-drizzle'; const builder = new SchemaBuilder<{ DrizzleSchema: typeof schema }>({ plugins: [drizzlePlugin], drizzle: { client: db }, }); builder.drizzleObject('users', { name: 'User', fields: t => ({ id: t.exposeID('id'), email: t.exposeString('email'), posts: t.relation('posts'), }), }); ``` ### DataLoader (manual) ```ts import DataLoader from 'dataloader'; function makeLoaders(db: DB) { return { postsByUser: new DataLoader(async (userIds) => { const posts = await db.posts.where('userId', 'in', userIds); const grouped = new Map(); for (const p of posts) { const arr = grouped.get(p.userId) ?? []; arr.push(p); grouped.set(p.userId, arr); } return userIds.map(id => grouped.get(id) ?? []); }), }; } // Per-request const yoga = createYoga({ context: ({ request }) => ({ user: ..., loaders: makeLoaders(db), }), }); ``` → Pothos + Prisma 가 자동. 자체 = manual loader. ### Subscription (real-time) ```ts builder.subscriptionType({ fields: t => ({ postCreated: t.field({ type: 'Post', subscribe: (_, __, ctx) => ctx.pubsub.subscribe('POST_CREATED'), resolve: (payload) => payload, }), }), }); ``` ```ts // Yoga + WebSocket import { createServer } from 'node:http'; import { useServer } from 'graphql-ws/lib/use/ws'; import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ server: httpServer, path: '/graphql' }); useServer({ schema, context: () => ({ ... }) }, wss); ``` ### Persisted queries ```ts import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'; const yoga = createYoga({ plugins: [ usePersistedOperations({ getPersistedOperation: (key) => operations[key], allowArbitraryOperations: false, // prod }), ], }); ``` → Client = hash, server = registered query. Bandwidth + security. ### Cost analysis (DoS 방지) ```ts import { useCostAnalysis } from '@envelop/cost-analysis'; const yoga = createYoga({ plugins: [ useCostAnalysis({ maximumCost: 1000, defaultCost: 1, // 매 field 의 cost 정의 }), ], }); ``` → 큰 nested query (10 → 100 → 1000) 차단. ### Depth limit ```ts import { useDepthLimit } from '@envelop/depth-limit'; useDepthLimit({ maxDepth: 7 }); ``` ### Error masking ```ts import { useMaskedErrors } from '@envelop/core'; useMaskedErrors({ maskError: (error, message) => { if (error.extensions?.code === 'INTERNAL_ERROR') { return new Error('Internal server error'); } return error; }, }); ``` → Internal error 사용자에 자세 X. ### Authentication ```ts const yoga = createYoga({ context: async ({ request }) => { const token = request.headers.get('authorization')?.replace('Bearer ', ''); const user = token ? await verifyJwt(token) : null; return { user }; }, }); // Resolver resolve: (_, __, ctx) => { if (!ctx.user) throw new GraphQLError('UNAUTHORIZED', { extensions: { code: 'UNAUTHORIZED' } }); // ... } ``` ### Authorization (field-level) ```ts import AuthPlugin from '@pothos/plugin-scope-auth'; const builder = new SchemaBuilder<{ AuthScopes: { admin: boolean; loggedIn: boolean }; }>({ plugins: [AuthPlugin], authScopes: ({ user }) => ({ admin: user?.role === 'admin', loggedIn: !!user, }), }); builder.queryFields(t => ({ adminStats: t.field({ type: 'Stats', authScopes: { admin: true }, resolve: () => ..., }), })); ``` ### Federation (마이크로서비스) ```ts import { fastify } from 'fastify'; import { useApolloFederation } from '@graphql-yoga/apollo-federation'; const subgraph = builder.toSchema(); useApolloFederation({ subgraph }); // Gateway import { stitchSchemas } from '@graphql-tools/stitch'; const supergraph = stitchSchemas({ subschemas: [usersSubgraph, ordersSubgraph], }); ``` ### Edge runtime (Hono + Yoga) ```ts import { Hono } from 'hono'; import { createYoga } from 'graphql-yoga'; const yoga = createYoga({ schema }); const app = new Hono(); app.all('/graphql', (c) => yoga.fetch(c.req.raw, c.env)); export default app; ``` → Cloudflare Workers / Vercel Edge. ### Mercurius (Fastify GraphQL, fast) ```ts import Fastify from 'fastify'; import mercurius from 'mercurius'; const app = Fastify(); app.register(mercurius, { schema, resolvers }); ``` → Yoga 의 Fastify 대안 — 매우 빠름. ### Code-first vs Schema-first ``` Code-first (Pothos): + Type-safe (TS 가 schema 만듦) + Refactoring 쉬움 - Schema = code (다른 lang client 가 generate 필요) Schema-first (SDL .graphql 파일): + Schema 가 truth + 다른 lang 가 generate 가능 + Tools (codegen) 친화 - Type 가 다른 곳 — drift 가능 ``` → Pothos 추세. ### vs Apollo Server ``` Apollo: + 큰 ecosystem + Apollo Studio (managed) - 옛 (some legacy) Yoga: + Modern, 빠름 + Edge 호환 + 작은 bundle - 작은 community ``` ### Persisted queries (Apollo Persisted Queries) ```ts // Build-time: // Client 의 모든 query → hash → registry. // Runtime: // Client 가 hash 만 보냄. // Server 가 hash → query 조회. // 장점: // - Bandwidth 작음 // - Public schema 숨김 (allowlist) // - DoS 방지 ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Modern TS GraphQL | Yoga + Pothos | | Federation | Apollo / Yoga + tools | | Edge runtime | Yoga | | Fastify ecosystem | Mercurius | | Existing Apollo | 점진 migrate | | Schema-first 강 | GraphQL Codegen + Yoga | ## ❌ 안티패턴 - **Cost analysis 없음**: 큰 nested query DoS. - **N+1 무관심**: DataLoader 또는 plugin. - **Depth limit 없음**: deep query. - **모든 field public**: auth scope. - **Schema 자주 변경 — version 없음**: client 깨짐. - **Public schema 추가 + persisted X**: introspection leak. ## 🤖 LLM 활용 힌트 - Yoga + Pothos = modern stack. - Pothos + Prisma plugin 가 N+1 자동. - Cost / depth / scope auth 3종 항상. - Edge runtime 호환. ## 🔗 관련 문서 - [[Backend_GraphQL_Server_Patterns]] - [[Web_GraphQL_Client_Patterns]] - [[Backend_Hono_Modern]]