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:
- GraphQL over HTTP (POST, GET, multipart requests)
- Built-in subscriptions over WebSocket (SSE support included)
- File uploads following the GraphQL multipart specification
- Middleware / Plugin system via Envelop for cross-cutting concerns
- Compatibility with existing Apollo tooling and GraphQL ecosystem
- GraphiQL and GraphQL playground built in for testing
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:
- Simplicity: You can create a working server in just a few lines of code.
- Extensibility: The Envelop plugin system lets you add functionality without bloating your core logic.
- Real-time first: Subscriptions work seamlessly with WebSockets and SSE, essential for modern apps.
- Future-proof: It stays aligned with the latest GraphQL spec and community standards (like multipart uploads).
- Testing friendly: Yoga provides utilities to test your schema and operations programmatically.
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
useLoggerβ Logs every operation with timing.useGraphQLJitβ Compiles queries for faster execution.useParserCacheβ Caches parsed documents.useResponseCacheβ Caches entire responses.usePromMetricsβ Exports Prometheus metrics.useGenericAuthβ Authentication abstraction.useDepthLimitβ Prevents deep queries.useRateLimitβ Limits operation frequency.
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:
- Environment variables: Use them for configuration
(port, database URLs, secrets). Yoga respects
PORTby default. - Helmets and security: Add HTTP headers using
node:httpoptions or a reverse proxy (nginx). - CORS: Yoga has built-in CORS support; configure it via
corsoption or use the@graphql-yoga/plugin-cors. - Health checks: Expose a
/healthendpoint (Yoga automatically serves one at/health). - Logging and monitoring: Integrate
useLoggerand export Prometheus metrics withusePromMetrics. - Performance: Enable
useGraphQLJitand response caching for high-load scenarios. - Subscriptions in production: Use a stable pub/sub (Redis, Kafka) and ensure WebSocket support on your hosting platform.
- Graceful shutdown: Yoga server supports
stop()to drain connections; wire it to SIGTERM.
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.