Files
2nd/10_Wiki/Topics/Coding/Backend_GraphQL_Yoga_Pothos.md
T
2026-05-09 22:47:42 +09:00

9.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-graphql-yoga-pothos GraphQL Yoga / Pothos — Modern GraphQL Server Coding draft B conceptual 2026-05-09 2026-05-09
backend
graphql
yoga
pothos
vibe-coding
language applicable_to
TS
Backend
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 시작

yarn add graphql graphql-yoga @pothos/core
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,
  }),
});
// Server (Bun / Node)
import { createServer } from 'node:http';
const server = createServer(yoga);
server.listen(4000);

Type-safe input

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

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

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)

import DataLoader from 'dataloader';

function makeLoaders(db: DB) {
  return {
    postsByUser: new DataLoader<string, Post[]>(async (userIds) => {
      const posts = await db.posts.where('userId', 'in', userIds);
      const grouped = new Map<string, Post[]>();
      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)

builder.subscriptionType({
  fields: t => ({
    postCreated: t.field({
      type: 'Post',
      subscribe: (_, __, ctx) => ctx.pubsub.subscribe('POST_CREATED'),
      resolve: (payload) => payload,
    }),
  }),
});
// 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

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 방지)

import { useCostAnalysis } from '@envelop/cost-analysis';

const yoga = createYoga({
  plugins: [
    useCostAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      // 매 field 의 cost 정의
    }),
  ],
});

→ 큰 nested query (10 → 100 → 1000) 차단.

Depth limit

import { useDepthLimit } from '@envelop/depth-limit';

useDepthLimit({ maxDepth: 7 });

Error masking

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

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)

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 (마이크로서비스)

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)

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)

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)

// 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 호환.

🔗 관련 문서