← Back to DevBytes

GraphQL Yoga from Beginner to Expert: A Learning Path

Introduction

GraphQL Yoga is a fully-featured, production-ready GraphQL server for Node.js. It combines the flexibility of the official graphql-js reference implementation with the powerful plugin architecture of Envelop, giving you everything you need to build robust, real-time APIs. This tutorial takes you from absolute beginner to expert, covering core concepts, practical examples, advanced features, and best practices along the way.

What is GraphQL Yoga?

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Created and maintained by The Guild, GraphQL Yoga is a modern GraphQL server that prioritises developer experience without sacrificing performance or capability. It wraps graphql-js and @envelop/core into a single, easy-to-use server. Out of the box, you get:

Because it's based on Envelop, you can tap into a rich set of plugins for logging, caching, tracing, authentication, and more, making it an ideal choice for projects of any size.

Why GraphQL Yoga Matters

Choosing the right GraphQL server affects your development speed, maintenance overhead, and the performance of your API. Here’s why Yoga stands out:

Getting Started: Installation and Project Setup

First, create a new Node.js project and install the required dependencies. GraphQL Yoga uses graphql-yoga as the main package, which automatically brings in graphql as a peer dependency.

mkdir my-yoga-server
cd my-yoga-server
npm init -y
npm install graphql-yoga graphql

You can also use Yarn or pnpm. The package provides both CommonJS and ESM exports. For modern projects, ESM is recommended. Add "type": "module" in your package.json to use import statements natively.

A Minimal Server

Let's create the simplest possible server. Create a file named index.js:

// index.js (ESM)
import { createServer } from 'graphql-yoga'

const server = createServer({
  schema: {
    typeDefs: `
      type Query {
        hello: String!
      }
    `,
    resolvers: {
      Query: {
        hello: () => 'Hello from Yoga!'
      }
    }
  }
})

server.start(() => {
  console.log('Server is running on http://localhost:4000')
})

Run node index.js and open http://localhost:4000. You'll see the GraphiQL playground. Execute { hello } and you'll get your first response.

Defining Your First Schema and Resolvers

A GraphQL schema defines the types and operations your API exposes. Yoga accepts a schema as either a string (SDL) or an executable schema created with graphql-tools. For most real-world use cases, you'll want to separate type definitions and resolvers into dedicated files.

Using SDL and Resolvers Object

// schema.js
export const typeDefs = `
  type Book {
    id: ID!
    title: String!
    author: String!
    publishedYear: Int
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
  }

  type Mutation {
    addBook(title: String!, author: String!, publishedYear: Int): Book!
  }
`

// Dummy data store
const books = [
  { id: '1', title: 'GraphQL in Action', author: 'Samer Buna', publishedYear: 2021 },
  { id: '2', title: 'Learning GraphQL', author: 'Eve Porcello', publishedYear: 2018 }
]

export const resolvers = {
  Query: {
    books: () => books,
    book: (parent, args) => books.find(b => b.id === args.id)
  },
  Mutation: {
    addBook: (parent, args) => {
      const newBook = {
        id: String(books.length + 1),
        title: args.title,
        author: args.author,
        publishedYear: args.publishedYear || null
      }
      books.push(newBook)
      return newBook
    }
  }
}

Then integrate the schema into the server:

import { createServer } from 'graphql-yoga'
import { typeDefs, resolvers } from './schema.js'

const server = createServer({
  schema: { typeDefs, resolvers }
})

server.start(() => console.log('Server ready'))

Using GraphQL Tools to Build an Executable Schema

For more complex setups (schema stitching, transforms), you can use @graphql-tools/schema. Yoga accepts any executable schema:

import { createServer } from 'graphql-yoga'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs, resolvers } from './schema.js'

const schema = makeExecutableSchema({ typeDefs, resolvers })
const server = createServer({ schema })

server.start()

Context, Middleware, and Authentication

The context is an object shared across all resolvers in a single request. It's perfect for passing authentication info, database connections, or user-specific data.

Creating Context

Yoga allows you to define a context factory function that receives the incoming HTTP request and returns the context object.

import { createServer } from 'graphql-yoga'

const server = createServer({
  schema: { typeDefs, resolvers },
  context: async ({ request }) => {
    // Extract token from Authorization header
    const authHeader = request.headers.get('authorization')
    const token = authHeader?.replace('Bearer ', '')
    
    // Simulate fetching user from token (replace with real logic)
    const user = token ? await fetchUserFromToken(token) : null
    return { user }
  }
})

In your resolvers, the third argument (often named contextValue) contains this object:

const resolvers = {
  Query: {
    me: (parent, args, context) => {
      if (!context.user) throw new Error('Not authenticated')
      return context.user
    }
  }
}

Envelop Plugins as Middleware

Yoga uses Envelop under the hood, giving you access to a plugin pipeline. You can use built-in plugins or create custom ones. For example, useGenericAuth simplifies authentication:

import { createServer } from 'graphql-yoga'
import { useGenericAuth } from '@envelop/generic-auth'

const server = createServer({
  schema: { typeDefs, resolvers },
  plugins: [
    useGenericAuth({
      // Custom logic to resolve user from context
      resolveUserFn: async (context) => {
        // context.request is available here
        const token = context.request.headers.get('authorization')
        return token ? verifyToken(token) : null
      },
      // Protect the whole schema by default
      protectAll: false,
      // Use @auth directive or similar
    })
  ]
})

Envelop plugins are the recommended way to handle cross-cutting concerns like logging, error masking, caching, and tracing. We'll explore more plugins later.

Subscriptions: Real-Time Data

Yoga supports GraphQL subscriptions over WebSocket with the graphql-transport-ws protocol. SSE (Server-Sent Events) is also available for simpler setups. You just need to define subscription resolvers.

Adding a Subscription

Extend your schema with a Subscription type and a resolver that returns an AsyncIterator. Yoga integrates seamlessly with @graphql-tools for pub/sub patterns.

// schema.js additions
export const typeDefs = `
  // ... previous types

  type Subscription {
    bookAdded: Book!
  }
`

// We'll use a simple in-memory pub/sub
import { PubSub } from 'graphql-yoga'

const pubSub = new PubSub()

export const resolvers = {
  // ... previous Query/Mutation
  Mutation: {
    addBook: (parent, args) => {
      const newBook = { id: String(books.length + 1), ...args }
      books.push(newBook)
      pubSub.publish('BOOK_ADDED', { bookAdded: newBook })
      return newBook
    }
  },
  Subscription: {
    bookAdded: {
      subscribe: () => pubSub.subscribe('BOOK_ADDED'),
      resolve: (payload) => payload.bookAdded
    }
  }
}

In the Yoga server, subscriptions work out-of-the-box. When you start the server, a WebSocket endpoint is available at the same path (e.g., /graphql). The GraphiQL playground automatically detects and allows subscription testing.

For production, you may want to use an external pub/sub like Redis or Google Cloud PubSub via @graphql-yoga/pubsub-redis or similar.

File Uploads with GraphQL Yoga

Yoga implements the GraphQL multipart request specification, enabling file uploads out of the box. You define a scalar for uploads (usually Upload) and use it in mutations.

// schema additions
type Mutation {
  uploadBookCover(bookId: ID!, file: Upload!): String!
}

The Upload scalar is automatically provided by Yoga, so you don't need to define it. In the resolver, the file argument is an object with methods like createReadStream().

import { createWriteStream } from 'fs'
import { join } from 'path'
import { finished } from 'stream/promises'

const resolvers = {
  Mutation: {
    uploadBookCover: async (parent, { bookId, file }) => {
      // file is a Promise that resolves to an Upload object
      const upload = await file
      const stream = upload.createReadStream()
      const savePath = join('./covers', `${bookId}-${upload.filename}`)
      
      // Pipe to local file (example; use cloud storage in production)
      const out = createWriteStream(savePath)
      stream.pipe(out)
      await finished(out)
      
      return `Cover saved to ${savePath}`
    }
  }
}

Clients can send multipart/form-data requests with the file and a JSON operations map. Yoga handles the parsing automatically.

Error Handling and Validation

Yoga provides a structured way to handle errors through Envelop plugins and custom error formatters. You can mask internal errors, add extensions, and implement validation logic.

Custom Error Masking

By default, Yoga masks unexpected errors to prevent leaking sensitive information. You can customize this behavior:

import { createServer, createGraphQLError } from 'graphql-yoga'

const server = createServer({
  schema: { typeDefs, resolvers },
  maskedErrors: {
    // Only expose errors with a certain flag
    maskError: (error, message, isGraphQLError) => {
      // If it's a custom error we want to expose, return the original
      if (error.extensions?.code === 'MY_CUSTOM_ERROR') {
        return error
      }
      // Otherwise, mask with a generic message
      return createGraphQLError('Something went wrong', {
        extensions: { code: 'INTERNAL_SERVER_ERROR' }
      })
    }
  }
})

To throw a custom error in resolvers, use createGraphQLError:

import { createGraphQLError } from 'graphql-yoga'

throw createGraphQLError('Book not found', {
  extensions: { code: 'NOT_FOUND', bookId: args.id }
})

Input Validation

You can use libraries like zod or joi inside resolvers, or integrate validation as an Envelop plugin. For example, the useValidation plugin from Envelop works with Yoga:

import { useValidation, validateZod } from '@envelop/validation'
import { z } from 'zod'

const addBookSchema = z.object({
  title: z.string().min(1),
  author: z.string().min(1),
  publishedYear: z.number().int().optional()
})

const server = createServer({
  schema: { typeDefs, resolvers },
  plugins: [
    useValidation({
      rules: [
        validateZod({
          schema: addBookSchema,
          // target the 'addBook' argument
          operation: 'addBook'
        })
      ]
    })
  ]
})

Plugins and Envelop Integration (Expert Level)

Yoga is built on Envelop, a plugin system that wraps the GraphQL execution pipeline. You can use dozens of community plugins or write your own to extend functionality.

Commonly Used Envelop Plugins

You add them via the plugins array:

import { useLogger } from '@envelop/logger'
import { createServer } from 'graphql-yoga'

const server = createServer({
  schema: { typeDefs, resolvers },
  plugins: [
    useLogger({
      logFn: (data) => console.log(`Operation: ${data.operationName} took ${data.duration}ms`)
    })
  ]
})

Writing a Custom Envelop Plugin

A plugin is an object with lifecycle hooks like onExecute, onParse, onContextBuilding. Here's a simple timing plugin:

const useTimingPlugin = () => ({
  onExecute: ({ args }) => {
    const start = Date.now()
    return {
      onExecuteDone: () => {
        const ms = Date.now() - start
        console.log(`Execution took ${ms}ms`)
      }
    }
  }
})

const server = createServer({
  schema: { typeDefs, resolvers },
  plugins: [useTimingPlugin()]
})

Testing Your GraphQL API

Yoga provides a buildSchema and createYoga utility, but you can test your schema directly without an HTTP server. For integration tests, Yoga ships a fetch-compatible server that works with Node.js fetch API.

Unit Testing Resolvers

Test resolvers by invoking them directly with mock arguments:

import { resolvers } from './schema.js'

test('books resolver returns list', () => {
  const result = resolvers.Query.books(null, {}, {})
  expect(result).toHaveLength(2)
})

Integration Testing via HTTP

Use the built-in server to test full request/response cycles:

import { createServer } from 'graphql-yoga'
import { typeDefs, resolvers } from './schema.js'

test('query books via HTTP', async () => {
  const yoga = createServer({ schema: { typeDefs, resolvers } })
  // Start server on random port
  const server = yoga.start()
  
  const response = await yoga.fetch('http://localhost:4000/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: '{ books { title } }' })
  })
  
  const { data } = await response.json()
  expect(data.books).toBeDefined()
  
  server.stop()
})

The yoga.fetch method is a convenience for testing; you can also use Node's native fetch once the server is running.

Production Deployment Best Practices

When moving to production, consider the following:

Example production configuration snippet:

import { createServer } from 'graphql-yoga'
import { useLogger } from '@envelop/logger'
import { useGraphQLJit } from '@envelop/graphql-jit'

const server = createServer({
  schema: { typeDefs, resolvers },
  plugins: [
    useLogger(),
    useGraphQLJit()
  ],
  cors: {
    origin: ['https://myapp.com'],
    credentials: true
  }
})

server.start(() => console.log('Production server ready'))

// Graceful shutdown
process.on('SIGTERM', () => {
  server.stop().then(() => process.exit(0))
})

Conclusion

GraphQL Yoga offers a streamlined, powerful, and extensible way to build GraphQL APIs. Starting from a simple hello query, you've seen how to define schemas, implement queries and mutations, add real-time subscriptions, handle file uploads, secure your API with context and authentication, leverage the Envelop plugin ecosystem, test your server, and prepare for production. Its alignment with the GraphQL specification and the Envelop standard ensures your skills transfer across the ecosystem while giving you the fastest path from idea to production. Continue experimenting with plugins, explore advanced topics like schema stitching and federation, and join the vibrant community around The Guild's tools to become a true GraphQL Yoga expert.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles