# GraphQL Yoga Server: Fully-Featured GraphQL Server

**GraphQL Yoga Server: A Fully‑Featured GraphQL Server**  
*Your one‑stop guide to building, scaling, and maintaining a production‑ready GraphQL API.*

---

## Hook  

> “Imagine a GraphQL server that works out‑of‑the‑box, gives you real‑time subscriptions, powerful error handling, and a developer‑experience that feels like a warm‑up lap. That’s GraphQL Yoga – the Swiss‑army‑knife of GraphQL back‑ends, designed to let you focus on business logic while it takes care of the plumbing.”

If you’ve ever wrestled with boilerplate, tangled middleware, or missing features in other GraphQL servers, Yoga will feel like a breath of fresh air. In the next ~1,600 words we’ll walk through **why Yoga exists, how to get it running in minutes, five proven patterns that make your API robust, and the FAQs you’ll hit first‑time users ask**. By the end you’ll have a production‑ready server you can drop into any Node.js project.

---

## Table of Contents  

1. [What Is GraphQL Yoga?](#what-is-graphql-yoga)  
2. [Getting Started – Installation & Basic Setup](#getting-started)  
3. [Deep‑Dive: Core Configuration Options](#core-config)  
4. [Five Essential Patterns]  
   - 4.1 [Modularising the Schema & Resolvers](#pattern-modular)  
   - 4.2 [Real‑Time Subscriptions](#pattern-subscriptions)  
   - 4.3 [Authentication & Authorization](#pattern-auth)  
   - 4.4 [Centralised Error Handling](#pattern-errors)  
   - 4.5 [Batching & Caching with DataLoader](#pattern-dataloader)  
5. [Frequently Asked Questions](#faq)  
6. [Conclusion & Next Steps](#conclusion)  

---

## 1. What Is GraphQL Yoga? <a name="what-is-graphql-yoga"></a>

GraphQL Yoga is an **opinionated, batteries‑included server** built on top of the `graphql-js` reference implementation, `express` (or any compatible HTTP framework), and `graphql-ws` for subscriptions. It was created by the team behind Prisma and aims to solve three pain points that repeatedly surface in GraphQL projects:

| Pain Point | How Yoga Solves It |
|------------|-------------------|
| **Missing features** (subscriptions, file uploads, playground) | All are enabled by default, configurable via a single `YogaServer` constructor. |
| **Fragmented middleware** (separate auth, logging, error handling) | Yoga’s `plugins` system lets you compose middleware in a predictable order. |
| **Scalability concerns** (caching, batching, schema stitching) | Built‑in support for `DataLoader`, schema stitching, and easy integration with Apollo Federation. |

Because Yoga is **framework‑agnostic**, you can run it on plain Node, inside an AWS Lambda, on Cloudflare Workers, or as part of a larger Express/Koa app. The API surface is intentionally small: a single `createYoga` call returns an HTTP handler that you can mount wherever you like.

---

## 2. Getting Started – Installation & Basic Setup <a name="getting-started"></a>

### 2.1 Install the package

```bash
# Using npm
npm install graphql-yoga

# Or with Yarn / pnpm
yarn add graphql-yoga
# pnpm add graphql-yoga
```

> **Tip:** Yoga pulls in `graphql` as a peer dependency, so you’ll get the latest stable version automatically.

### 2.2 Minimal “Hello World” server

Create a file called `server.js` (or `index.ts` if you prefer TypeScript) and paste the following:

```js
// server.js
import { createYoga, createSchema } from 'graphql-yoga';

// 1️⃣ Define the type definitions (SDL)
const typeDefs = /* GraphQL */ `
  type Query {
    hello: String!
  }
`;

// 2️⃣ Implement resolvers
const resolvers = {
  Query: {
    hello: () => '👋 Hello, GraphQL Yoga!',
  },
};

// 3️⃣ Build the executable schema
const schema = createSchema({
  typeDefs,
  resolvers,
});

// 4️⃣ Create the Yoga server
const yoga = createYoga({
  schema,
  // Optional: enable GraphQL Playground in dev
  graphqlEndpoint: '/graphql',
  // Turn on CORS for local testing
  cors: {
    origin: '*',
    credentials: true,
  },
});

// 5️⃣ Export the handler (works with Node, Vercel, Netlify, etc.)
export default yoga;
```

Run it with Node:

```bash
node server.js
```

Visit `http://localhost:4000/graphql` and you’ll see the interactive Playground. Run the query:

```graphql
query {
  hello
}
```

You should receive:

```json
{
  "data": {
    "hello": "👋 Hello, GraphQL Yoga!"
  }
}
```

That’s it—your first GraphQL API is live in **under a minute**.

---

## 3. Deep‑Dive: Core Configuration Options <a name="core-config"></a>

While the minimal example works, real‑world services need more knobs. Yoga’s `createYoga` accepts a rich options object; the most useful sections are:

| Option | Description | Example |
|--------|-------------|---------|
| `schema` | The executable GraphQL schema (required). | `createSchema({ typeDefs, resolvers })` |
| `plugins` | Array of plugins for logging, auth, tracing, etc. | `[useGraphQLModules(), useApolloTracing()]` |
| `graphqlEndpoint` | Path for the HTTP GraphQL endpoint. | `'/api/graphql'` |
| `landingPage` | Choose Playground, GraphiQL, or a custom UI. | `true` (auto‑detect) |
| `cors` | CORS configuration – can be a boolean or object. | `{ origin: ['https://myapp.com'], credentials: true }` |
| `healthCheckEndpoint` | Simple health‑check route (useful for k8s). | `'/health'` |
| `maskedErrors` | Hide internal error details from clients (production). | `true` |
| `context` | Function that builds the per‑request context (auth, DB, loaders). | `({ request }) => ({ user: getUser(request) })` |
| `subscriptions` | Enable/disable WebSocket subscriptions; configure path. | `{ path: '/graphql/ws' }` |
| `validationRules` | Add custom GraphQL validation rules. | `[depthLimit(10)]` |

**Example: Production‑ready configuration**

```js
import { createYoga, createSchema } from 'graphql-yoga';
import depthLimit from 'graphql-depth-limit';
import { useApolloTracing } from '@graphql-yoga/plugin-apollo-tracing';
import { getUserFromToken } from './auth';
import { createLoaders } from './loaders';

const yoga = createYoga({
  schema,
  plugins: [useApolloTracing()],
  graphqlEndpoint: '/api/graphql',
  landingPage: false, // disable Playground in prod
  cors: {
    origin: ['https://myapp.com'],
    credentials: true,
  },
  healthCheckEndpoint: '/healthz',
  maskedErrors: true,
  validationRules: [depthLimit(8)],
  context: async ({ request }) => {
    const token = request.headers.get('authorization')?.replace('Bearer ', '');
    const user = await getUserFromToken(token);
    const loaders = createLoaders(); // DataLoader instances
    return { user, loaders };
  },
});
```

All of the above can be mixed and matched; Yoga’s philosophy is “opt‑out of defaults you don’t need, opt‑in to the ones you do”.

---

## 4. Five Essential Patterns <a name="patterns"></a>

Below are five patterns that turn a simple Yoga server into a **maintainable, secure, and high‑performance API**. Each pattern includes a short rationale, code snippets, and best‑practice tips.

### 4.1 Modularising the Schema & Resolvers <a name="pattern-modular"></a>

**Why?** As your API grows, a monolithic `typeDefs` string becomes unwieldy. Splitting the schema into feature modules (e.g., `User`, `Post`, `Comment`) improves readability, enables independent testing, and plays nicely with code‑generation tools.

**Implementation**

```
src/
 ├─ schema/
 │   ├─ index.js          // aggregates modules
 │   ├─ user.graphql
 │   ├─ post.graphql
 │   └─ comment.graphql
 └─ resolvers/
     ├─ index.js
     ├─ user.js
     ├─ post.js
     └─ comment.js
```

**`src/schema/index.js`**

```js
import { readFileSync } from 'fs';
import { join } from 'path';

export const typeDefs = [
  readFileSync(join(__dirname, 'user.graphql'), 'utf8'),
  readFileSync(join(__dirname, 'post.graphql'), 'utf8'),
  readFileSync(join(__dirname, 'comment.graphql'), 'utf8'),
].join('\n');
```

**`src/resolvers/index.js`**

```js
import userResolvers from './user.js';
import postResolvers from './post.js';
import commentResolvers from './comment.js';

export const resolvers = {
  Query: {
    ...userResolvers.Query,
    ...postResolvers.Query,
    ...commentResolvers.Query,
  },
  Mutation: {
    ...userResolvers.Mutation,
    ...postResolvers.Mutation,
    ...commentResolvers.Mutation,
  },
  // If you have custom scalar or enum resolvers, spread them here too.
};
```

**Hooking into Yoga**

```js
import { createYoga, createSchema } from 'graphql-yoga';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';

const schema = createSchema({ typeDefs, resolvers });
const yoga = createYoga({ schema });
```

**Best Practices**

* Keep each `.graphql` file **self‑contained** (type definitions, input types, enums).  
* Export resolvers as **named objects** (`Query`, `Mutation`, `Subscription`, custom types).  
* Use a lint rule like `graphql/template-strings` to catch duplicate type names early.

---

### 4.2 Real‑Time Subscriptions <a name="pattern-subscriptions"></a>

**Why?** Modern apps (chat, dashboards, collaborative editing) need push updates. Yoga bundles `graphql-ws` under the hood, giving you a standards‑compliant subscription endpoint with minimal code.

**Schema**

```graphql
# src/schema/subscription.graphql
type Subscription {
  messageAdded(roomId: ID!): Message!
}
```

**Resolver**

```js
// src/resolvers/subscription.js
import { PubSub } from 'graphql-yoga';

const pubsub = new PubSub(); // In‑memory; replace with Redis for scaling

export const Subscription = {
  messageAdded: {
    subscribe: (_, { roomId }) => pubsub.subscribe(`ROOM_${roomId}`),
  },
};

export const Mutation = {
  addMessage: async (_, { roomId, content }, { user }) => {
    const message = await createMessage({ roomId, content, authorId: user.id });
    await pubsub.publish(`ROOM_${roomId}`, { messageAdded: message });
    return message;
  },
};
```

**Mounting the subscription server**

```js
const yoga = createYoga({
  schema,
  // The `subscriptions` key enables the WS endpoint
  subscriptions: {
    path: '/graphql/subscriptions',
    // Optional: pass a custom `onConnect` hook for auth
    onConnect: async (ctx) => {
      const token = ctx.connectionParams?.authToken;
      const user = await getUserFromToken(token);
      if (!user) throw new Error('Authentication required');
      return { user };
    },
  },
});
```

**Client Example (Apollo Client)**

```js
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({ uri: '/api/graphql' });
const wsLink = new GraphQLWsLink(
  createClient({ url: 'ws://localhost:4000/graphql/subscriptions' })
);

const splitLink = split(
  ({ query }) => {
    const def = getMainDefinition(query);
    return def.kind === 'OperationDefinition' && def.operation === 'subscription';
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});
```

**Scaling Tip**  
For multi‑instance deployments, replace the in‑memory `PubSub` with a **Redis** or **NATS** adapter. Yoga’s `createPubSub` factory accepts a custom implementation, letting you keep the same resolver code while swapping the transport.

---

### 4.3 Authentication & Authorization <a name="pattern-auth"></a>

**Why?** Every production API must verify the caller and enforce permissions. Yoga’s `context` function runs **once per request** (or per WebSocket connection) and is the perfect place to decode JWTs, fetch user records, and attach a `user` object to the resolver chain.

**JWT‑based Context**

```js
// src/context.js
import { verify } from 'jsonwebtoken';
import { getUserById } from './db';

export async function createContext({ request, connection }) {
  // HTTP request
  if (request) {
    const auth = request.headers.get('authorization')?.replace('Bearer ', '');
    if (!auth) return { user: null };
    try {
      const payload = verify(auth, process.env.JWT_SECRET);
      const user = await getUserById(payload.sub);
      return { user };
    } catch (e) {
      return { user: null };
    }
  }

  // WebSocket connection (subscriptions)
  if (connection) {
    const { authToken } = connection.context;
    if (!authToken) return { user: null };
    const payload = verify(authToken, process.env.JWT_SECRET);
    const user = await getUserById(payload.sub);
    return { user };
  }

  return { user: null };
}
```

**Hooking into Yoga**

```js
import { createYoga } from 'graphql-yoga';
import { createContext } from './context';

const yoga = createYoga({
  schema,
  context: createContext,
});
```

**Authorization in Resolvers**

```js
// src/resolvers/post.js
export const Query = {
  myPosts: async (_, __, { user }) => {
    if (!user) throw new Error('Unauthenticated');
    return db.post.findMany({ where: { authorId: user.id } });
  },
};

export const Mutation = {
  deletePost: async (_, { postId }, { user }) => {
    const post = await db.post.findUnique({ where: { id: postId } });
    if (!post) throw new Error('Post not found');
    if (post.authorId !== user.id) throw new Error('Not authorized');
    return db.post.delete({ where: { id: postId } });
  },
};
```

**Pattern Tips**

* **Fail fast** – throw early if `!user`. Yoga will convert the error to a GraphQL error (masked in prod).  
* **Role‑based checks** – add a `role` field to the user object and write a helper `hasRole(user, role)`.  
* **Field‑level security** – for complex objects, use resolver wrappers (`withAuth`) that filter fields based on permissions.

---

### 4.4 Centralised Error Handling <a name="pattern-errors"></a>

**Why?** GraphQL returns a **partial data** payload when an error occurs. Without a consistent strategy, you’ll end up with a mix of stack traces, raw DB errors, and ambiguous messages that leak implementation details.

**Yoga’s built‑in error formatter**

```js
import { createYoga } from 'graphql-yoga';
import { GraphQLError } from 'graphql';

function formatError(error) {
  // Preserve original error for logging
  console.error('[GraphQL Error]', error);

  // Hide internal details in production
  if (process.env.NODE_ENV === 'production') {
    return new GraphQLError('Internal server error');
  }

  // In dev, return the full error with extensions
  return {
    message: error.message,
    locations: error.locations,
    path: error.path,
    extensions: {
      code: error.extensions?.code ?? 'INTERNAL_SERVER_ERROR',
      // Include stack trace only in dev
      stack: error.stack,
    },
  };
}

const yoga = createYoga({
  schema,
  maskedErrors: false, // we’ll handle masking ourselves
  formatError,
});
```

**Custom Error Classes**

```js
export class AuthError extends Error {
  constructor(message = 'Authentication required') {
    super(message);
    this.name = 'AuthError';
    this.extensions = { code: 'UNAUTHENTICATED' };
  }
}

export class ForbiddenError extends Error {
  constructor(message = 'You are not allowed to perform this action') {
    super(message);
    this.name = 'ForbiddenError';
    this.extensions = { code: 'FORBIDDEN' };
  }
}
```

**Using the custom errors**

```js
export const Mutation = {
  updateProfile: async (_, { input }, { user }) => {
    if (!user) throw new AuthError();
    if (!user.canEditProfile) throw new ForbiddenError();
    // ... update logic
  },
};
```

**Result**

* **Consistent `extensions.code`** – clients can map errors to UI states.  
* **No stack traces in production** – the formatter strips them out.  
* **Central logging** – all errors funnel through a single `console.error` (replace with Winston, Pino, or a cloud logger).

---

### 4.5 Batching & Caching with DataLoader <a name="pattern-dataloader"></a>

**Why?** The N+1 problem is notorious in GraphQL: a resolver that fetches a list of posts may trigger a separate DB query for each author. `DataLoader` batches those calls into a single request per request‑cycle, dramatically reducing latency.

**Create a loader factory**

```js
// src/loaders.js
import DataLoader from 'dataloader';
import { db } from './db';

export function createLoaders() {
  return {
    userById: new DataLoader(async (ids) => {
      const users = await db.user.findMany({ where: { id: { in: ids } } });
      // Preserve order of ids
      const userMap = new Map(users.map((u) => [u.id, u]));
      return ids.map((id) => userMap.get(id) ?? null);
    }),

    // Example: batch loading comments for many posts
    commentsByPostId: new DataLoader(async (postIds) => {
      const comments = await db.comment.findMany({
        where: { postId: { in: postIds } },
      });
      const commentMap = postIds.map((pid) =>
        comments.filter((c) => c.postId === pid)
      );
      return commentMap;
    }),
  };
}
```

**Inject loaders via context**

```js
import { createContext } from './context';
import { createLoaders } from './loaders';

const yoga = createYoga({
  schema,
  context: async (params) => {
    const base = await createContext(params);
    return { ...base, loaders: createLoaders() };
  },
});
```

**Use loaders in resolvers**

```js
export const Post = {
  author: async (parent, _, { loaders }) => {
    // `parent.authorId` is the foreign key
    return loaders.userById.load(parent.authorId);
  },

  comments: async (parent, _, { loaders }) => {
    return loaders.commentsByPostId.load(parent.id);
  },
};
```

**Advanced Tips**

* **Cache per request** – instantiate loaders inside the `context` function so each request gets a fresh cache.  
* **Prime the cache** – after creating a new record, call `loader.prime(id, record)` to avoid a second round‑trip.  
* **Avoid over‑caching** – for rarely‑changed data, you can enable `cache: false` on a loader and rely on external CDN or Redis caching.

---

## 5. Frequently Asked Questions <a name="faq"></a>

| # | Question | Answer |
|---|----------|--------|
| **1** | *Do I need to run a separate HTTP server?* | No. `createYoga` returns a handler that can be mounted on any Node HTTP server (Express, Fastify, Koa) **or** exported directly for serverless platforms (Vercel, Netlify, AWS Lambda). |
| **2** | *Can Yoga work with TypeScript?* | Absolutely. All core APIs ship with TypeScript definitions. You can write schema files as `.graphql` or use `gql` template literals. The `createYoga` return type is `RequestHandler` compatible with `@types/express`. |
| **3** | *How do I enable GraphQL Playground only in development?* | Set `landingPage: process.env.NODE_ENV !== 'production'` or use the `plugins` array: `[process.env.NODE_ENV === 'development' && useGraphQLPlayground()]`. |
| **4** | *What about file uploads?* | Yoga includes the `graphql-upload` middleware out of the box. Define an `Upload` scalar in your schema and use it in mutations. |
| **5** | *Is Yoga compatible with Apollo Federation?* | Yes. Use `createYoga({ schema, plugins: [useApolloFederation()] })` and expose the `_service` and `_entities` fields automatically. |
| **6** | *How do I scale subscriptions across multiple instances?* | Replace the default `PubSub` with a Redis or NATS adapter. Yoga’s `createPubSub` accepts a custom implementation, so your subscription resolvers stay unchanged. |
| **7** | *Can I add custom validation rules (e.g., query depth limit)?* | Provide an array to the `validationRules` option: `validationRules: [depthLimit(10), yourCustomRule]`. |
| **8** | *What’s the best way to monitor performance?* | Use the `@graphql-yoga/plugin-apollo-tracing` plugin or integrate with OpenTelemetry via the `useTracing` plugin. Combine with server‑level metrics (Prometheus, Grafana). |
| **9** | *How do I handle graceful shutdown?* | If you embed Yoga in an Express app, listen for `SIGTERM`/`SIGINT` and call `server.close()`; Yoga’s subscription server will close its WebSocket server automatically. |
| **10** | *Is there a way to hot‑reload schema during development?* | Yes. When using `nodemon` or `ts-node-dev`, any change to the imported `typeDefs` will cause Yoga to rebuild the schema on the next request. For faster feedback, enable `schemaCache: false`. |

---

## 6. Conclusion & Next Steps <a name="conclusion"></a>

GraphQL Yoga packs **everything you need to ship a production‑grade GraphQL API** into a single, ergonomic package. By following the steps above you will have:

1. **A runnable server in under a minute** – with Playground, CORS, and health‑check out of the box.  
2. **A modular codebase** that scales as your domain grows.  
3. **Real‑time capabilities** via WebSocket subscriptions, ready for collaborative apps.  
4. **Secure authentication & fine‑grained authorization** baked into the request context.  
5. **Consistent error handling** that protects internal details while giving clients actionable error codes.  
6. **Performance‑optimised data fetching** through DataLoader batching and optional caching layers.

From here you can:

* **Add federation** to join multiple micro‑services into a single graph.  
* **Swap the in‑memory PubSub for Redis** to support horizontal scaling.  
* **Integrate with a logging platform** (e.g., Winston → Datadog) for observability.  
* **Write automated schema tests** using `graphql-tester` or `apollo-server-testing`.  

Whether you’re building a simple blog API or a complex, multi‑tenant SaaS platform, GraphQL Yoga gives you a solid foundation that lets you focus on **business logic**, not boilerplate. Happy coding!
