Introduction to GraphQL Yoga with TypeScript
GraphQL Yoga is a fully-featured, batteries-included GraphQL server built on top of the modern web standards from the GraphQL Foundation. It provides an elegant way to build GraphQL APIs with TypeScript, offering first-class support for strong typing, automatic code generation, and seamless integration with the GraphQL ecosystem. When combined with TypeScript, GraphQL Yoga enables developers to create end-to-end type-safe GraphQL applications where the types flow from the database layer all the way to the client.
What is GraphQL Yoga?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →GraphQL Yoga is an open-source GraphQL server library that acts as a wrapper around the official GraphQL implementations. It supports multiple GraphQL transport protocols including HTTP, WebSockets, and Server-Sent Events. The library is built by The Guild, a group of expert GraphQL contributors, and is designed to be compatible with any GraphQL client, tooling, and runtime environment including Node.js, Deno, Bun, and Cloudflare Workers.
Key features of GraphQL Yoga include:
- Built-in Subscriptions — Supports real-time data over WebSockets and SSE
- File Uploads — GraphQL multipart request spec support out of the box
- Health Checks & Readiness — Production-grade observability endpoints
- Error Masking — Automatic sanitization of error details in production
- Persisted Queries — Support for automatic persisted queries (APQ)
- TypeScript-First Design — Excellent type inference and code generation capabilities
Why TypeScript + GraphQL Yoga Matters
Building GraphQL APIs without TypeScript leads to a disconnect between your schema types and your runtime code. You define your types in SDL (Schema Definition Language) strings, but your resolver functions operate on plain JavaScript objects with no guarantee that they match the schema. This disconnect causes:
- Runtime errors from mismatched resolver return types
- Frustrating debugging sessions chasing undefined properties
- No IDE autocompletion for resolver arguments or context
- Manual type maintenance across schema changes
With GraphQL Yoga and TypeScript, you achieve end-to-end type safety. Your GraphQL schema becomes the single source of truth, and TypeScript ensures every resolver, every context method, and every data transformation aligns with that schema at compile time. This dramatically reduces bugs and accelerates development velocity.
Setting Up a GraphQL Yoga TypeScript Project
Let's create a strongly typed GraphQL Yoga project from scratch. We'll use a book library API as our example domain.
Step 1: Initialize the Project
mkdir yoga-ts-books
cd yoga-ts-books
npm init -y
npm install graphql-yoga graphql typescript ts-node @types/node --save
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --module commonjs --target ES2020 --strict true
Step 2: Define Your Data Sources
Create a src/data.ts file with typed data and interfaces:
// src/data.ts
export interface Book {
id: string;
title: string;
authorId: string;
publishedYear: number;
genres: string[];
rating: number;
}
export interface Author {
id: string;
name: string;
nationality: string;
birthYear: number;
books: string[]; // array of book IDs
}
export interface Review {
id: string;
bookId: string;
reviewerName: string;
rating: number;
comment: string;
date: string;
}
// In-memory data store
export const books: Book[] = [
{
id: "b1",
title: "The Silent Horizon",
authorId: "a1",
publishedYear: 2020,
genres: ["science fiction", "mystery"],
rating: 4.5,
},
{
id: "b2",
title: "Echoes of Tomorrow",
authorId: "a1",
publishedYear: 2022,
genres: ["science fiction"],
rating: 4.2,
},
{
id: "b3",
title: "The Garden of Whispers",
authorId: "a2",
publishedYear: 2019,
genres: ["literary fiction", "romance"],
rating: 4.8,
},
{
id: "b4",
title: "Steel and Shadows",
authorId: "a3",
publishedYear: 2021,
genres: ["thriller", "crime"],
rating: 3.9,
},
];
export const authors: Author[] = [
{
id: "a1",
name: "Elena Voss",
nationality: "German",
birthYear: 1985,
books: ["b1", "b2"],
},
{
id: "a2",
name: "Marco Reyes",
nationality: "Mexican",
birthYear: 1978,
books: ["b3"],
},
{
id: "a3",
name: "Sarah Kim",
nationality: "South Korean",
birthYear: 1990,
books: ["b4"],
},
];
export const reviews: Review[] = [
{
id: "r1",
bookId: "b1",
reviewerName: "Alice Chen",
rating: 5,
comment: "A breathtaking journey through time and space. Couldn't put it down.",
date: "2023-01-15",
},
{
id: "r2",
bookId: "b1",
reviewerName: "Bob Martinez",
rating: 4,
comment: "Solid world-building but the ending felt rushed.",
date: "2023-02-20",
},
{
id: "r3",
bookId: "b3",
reviewerName: "Clara Johnson",
rating: 5,
comment: "Marco Reyes has crafted a masterpiece. Every page sings.",
date: "2023-03-10",
},
{
id: "r4",
bookId: "b4",
reviewerName: "David Park",
rating: 3,
comment: "Decent thriller but predictable twists.",
date: "2023-04-05",
},
];
Step 3: Create the GraphQL Schema with Code-First Approach
GraphQL Yoga supports both SDL-first and code-first approaches. For maximum type safety, the code-first approach using graphql schema builders is recommended. However, we'll use the SDL-first approach with TypeScript type definitions that mirror our schema, which is more common and equally powerful when paired with proper typing.
Create src/schema.ts with your type definitions:
// src/schema.ts
export const typeDefs = /* GraphQL */ `
type Book {
id: ID!
title: String!
author: Author!
publishedYear: Int!
genres: [String!]!
rating: Float!
reviews: [Review!]!
}
type Author {
id: ID!
name: String!
nationality: String!
birthYear: Int!
books: [Book!]!
}
type Review {
id: ID!
book: Book!
reviewerName: String!
rating: Int!
comment: String!
date: String!
}
type Query {
books: [Book!]!
book(id: ID!): Book
authors: [Author!]!
author(id: ID!): Author
booksByGenre(genre: String!): [Book!]!
topRatedBooks(minRating: Float!): [Book!]!
}
type Mutation {
addReview(bookId: ID!, reviewerName: String!, rating: Int!, comment: String!): Review!
updateBookRating(bookId: ID!, newRating: Float!): Book!
}
schema {
query: Query
mutation: Mutation
}
`;
Step 4: Define Strongly Typed Resolvers
Create src/resolvers.ts. This is where TypeScript shines — we explicitly type the resolver map to match our schema:
// src/resolvers.ts
import { books, authors, reviews, Book, Author, Review } from "./data.js";
// Define the shape of our GraphQL context
export interface AppContext {
requestId: string;
timestamp: Date;
}
// Define resolver argument types explicitly
interface BookArgs {
id: string;
}
interface AuthorArgs {
id: string;
}
interface BooksByGenreArgs {
genre: string;
}
interface TopRatedBooksArgs {
minRating: number;
}
interface AddReviewArgs {
bookId: string;
reviewerName: string;
rating: number;
comment: string;
}
interface UpdateBookRatingArgs {
bookId: string;
newRating: number;
}
export const resolvers = {
Query: {
books: (_parent: unknown, _args: unknown, context: AppContext): Book[] => {
console.log(`[${context.requestId}] Fetching all books`);
return books;
},
book: (_parent: unknown, args: BookArgs, context: AppContext): Book | null => {
console.log(`[${context.requestId}] Fetching book: ${args.id}`);
const book = books.find((b) => b.id === args.id);
return book || null;
},
authors: (_parent: unknown, _args: unknown, context: AppContext): Author[] => {
console.log(`[${context.requestId}] Fetching all authors`);
return authors;
},
author: (_parent: unknown, args: AuthorArgs, context: AppContext): Author | null => {
console.log(`[${context.requestId}] Fetching author: ${args.id}`);
const author = authors.find((a) => a.id === args.id);
return author || null;
},
booksByGenre: (_parent: unknown, args: BooksByGenreArgs, context: AppContext): Book[] => {
console.log(`[${context.requestId}] Filtering books by genre: ${args.genre}`);
const genreLower = args.genre.toLowerCase();
return books.filter((b) =>
b.genres.some((g) => g.toLowerCase() === genreLower)
);
},
topRatedBooks: (_parent: unknown, args: TopRatedBooksArgs, context: AppContext): Book[] => {
console.log(`[${context.requestId}] Fetching books with rating >= ${args.minRating}`);
return books.filter((b) => b.rating >= args.minRating).sort((a, b) => b.rating - a.rating);
},
},
Mutation: {
addReview: (_parent: unknown, args: AddReviewArgs, context: AppContext): Review => {
console.log(`[${context.requestId}] Adding review for book: ${args.bookId}`);
const newReview: Review = {
id: `r${reviews.length + 1}`,
bookId: args.bookId,
reviewerName: args.reviewerName,
rating: args.rating,
comment: args.comment,
date: new Date().toISOString().split("T")[0],
};
reviews.push(newReview);
// Update the book's average rating
const bookReviews = reviews.filter((r) => r.bookId === args.bookId);
const avgRating =
bookReviews.reduce((sum, r) => sum + r.rating, 0) / bookReviews.length;
const book = books.find((b) => b.id === args.bookId);
if (book) {
book.rating = Math.round(avgRating * 10) / 10;
}
return newReview;
},
updateBookRating: (_parent: unknown, args: UpdateBookRatingArgs, context: AppContext): Book => {
console.log(`[${context.requestId}] Updating rating for book: ${args.bookId}`);
const book = books.find((b) => b.id === args.bookId);
if (!book) {
throw new Error(`Book with id ${args.bookId} not found`);
}
book.rating = args.newRating;
return book;
},
},
// Type-level resolvers for relationships
Book: {
author: (parent: Book, _args: unknown, _context: AppContext): Author | null => {
const author = authors.find((a) => a.id === parent.authorId);
return author || null;
},
reviews: (parent: Book, _args: unknown, _context: AppContext): Review[] => {
return reviews.filter((r) => r.bookId === parent.id);
},
},
Author: {
books: (parent: Author, _args: unknown, _context: AppContext): Book[] => {
return books.filter((b) => parent.books.includes(b.id));
},
},
Review: {
book: (parent: Review, _args: unknown, _context: AppContext): Book | null => {
const book = books.find((b) => b.id === parent.bookId);
return book || null;
},
},
};
Step 5: Wire Everything Together in the Server
Create src/index.ts to bootstrap the Yoga server:
// src/index.ts
import { createYoga, createSchema } from "graphql-yoga";
import { createServer } from "node:http";
import { typeDefs } from "./schema.js";
import { resolvers, AppContext } from "./resolvers.js";
import { v4 as uuidv4 } from "uuid";
// Build the executable GraphQL schema
const schema = createSchema({
typeDefs,
resolvers,
});
// Create the Yoga instance with typed context
const yoga = createYoga({
schema,
context: (): AppContext => ({
requestId: uuidv4(),
timestamp: new Date(),
}),
// Enable GraphiQL in development
graphiql: true,
// Mask errors in production for security
maskedErrors: process.env.NODE_ENV === "production",
// Enable CORS for browser access
cors: {
origin: ["http://localhost:3000", "https://myapp.com"],
credentials: true,
},
// Logging for debugging
logging: true,
});
// Create the HTTP server
const server = createServer(yoga);
// Start listening
const port = process.env.PORT || 4000;
server.listen(port, () => {
console.log(`🚀 GraphQL Yoga server running on http://localhost:${port}/graphql`);
});
To handle the UUID dependency, install it:
npm install uuid @types/uuid --save
Step 6: Add TypeScript Build Scripts
Update your package.json with convenient scripts:
"scripts": {
"dev": "ts-node --esm src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
Advanced Type Safety with Code Generation
For larger projects, manually typing resolver arguments becomes tedious and error-prone. GraphQL Yoga integrates seamlessly with code generation tools like GraphQL Code Generator to automatically produce TypeScript types from your schema.
Setting Up GraphQL Code Generator
npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers --save-dev
Create codegen.ts configuration:
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./src/schema.ts",
generates: {
"./src/generated/types.ts": {
plugins: ["typescript", "typescript-resolvers"],
config: {
contextType: "../resolvers#AppContext",
mappers: {
Book: "../data#Book",
Author: "../data#Author",
Review: "../data#Review",
},
// Use ID type for all ID fields
scalars: {
ID: { input: "string", output: "string" },
},
},
},
},
};
export default config;
Run the code generator:
npx graphql-codegen
This produces a src/generated/types.ts file with fully typed resolver signatures. You can then refactor your resolvers to use these generated types:
// Example of using generated types in resolvers
import { Resolvers } from "./generated/types.js";
import { AppContext } from "../resolvers.js";
export const resolvers: Resolvers = {
Query: {
books: (_parent, _args, context) => {
// context is now typed as AppContext automatically
// return type is inferred as Book[] from the generated types
return books;
},
// ... rest of resolvers
},
};
Leveraging Yoga's Built-in Features with Type Safety
Subscriptions with Typed Events
GraphQL Yoga supports real-time subscriptions over WebSockets. Here's how to add a subscription for new reviews with full type safety:
// Add to src/schema.ts typeDefs
const subscriptionDefs = /* GraphQL */ `
type Subscription {
newReview(bookId: ID!): Review!
}
`;
// Add to src/resolvers.ts
import { createPubSub } from "graphql-yoga";
// Typed pub/sub events
interface PubSubEvents {
"NEW_REVIEW": [Review];
}
const pubSub = createPubSub();
export const subscriptionResolvers = {
Subscription: {
newReview: {
subscribe: (_parent: unknown, args: { bookId: string }, _context: AppContext) => {
return {
[Symbol.asyncIterator]() {
let unsubscribe: () => void;
return {
next: () => {
return new Promise>((resolve) => {
unsubscribe = pubSub.subscribe("NEW_REVIEW", (review) => {
if (review.bookId === args.bookId) {
unsubscribe();
resolve({ value: review, done: false });
}
});
});
},
return: () => {
if (unsubscribe) unsubscribe();
return Promise.resolve({ value: undefined, done: true });
},
throw: (error: Error) => {
if (unsubscribe) unsubscribe();
return Promise.reject(error);
},
};
},
};
},
},
},
};
// Update Mutation.addReview to publish events
// After creating newReview, add:
// pubSub.publish("NEW_REVIEW", newReview);
File Uploads with TypeScript
GraphQL Yoga supports the GraphQL multipart request specification for file uploads:
// Add to schema
const uploadDefs = /* GraphQL */ `
scalar File
type Mutation {
uploadBookCover(bookId: ID!, file: File!): String!
}
`;
// In resolvers with typed File from graphql-yoga
import { File } from "graphql-yoga";
interface UploadBookCoverArgs {
bookId: string;
file: File;
}
export const uploadResolvers = {
Mutation: {
uploadBookCover: async (_parent: unknown, args: UploadBookCoverArgs): Promise => {
const file = args.file;
const buffer = await file.arrayBuffer();
// Process the file buffer (e.g., save to storage)
const filename = `${args.bookId}-cover-${Date.now()}.${file.type.split("/")[1]}`;
// ... save logic here
return `https://storage.example.com/covers/${filename}`;
},
},
};
Best Practices for GraphQL Yoga with TypeScript
1. Use a Single Source of Truth for Types
Your GraphQL schema should be the authoritative source for type definitions. Use code generation to derive TypeScript types rather than maintaining parallel type definitions manually. This ensures your runtime resolvers always match your schema.
2. Type Your Context Rigorously
The context object is the dependency injection mechanism for your GraphQL resolvers. Define a clear AppContext interface and use Yoga's generic createYoga<T> to propagate that type through all resolvers:
interface AppContext {
db: DatabaseConnection;
user?: AuthenticatedUser;
loaders: DataLoaderRegistry;
}
const yoga = createYoga({
context: async ({ request }): Promise => ({
db: await connectToDatabase(),
user: await authenticateUser(request),
loaders: createDataLoaders(),
}),
});
3. Implement DataLoaders for N+1 Problem Prevention
TypeScript helps you build type-safe DataLoaders that batch database queries efficiently:
import DataLoader from "dataloader";
export function createDataLoaders() {
return {
authorLoader: new DataLoader(async (ids) => {
const result = await db.authors.findByIds(ids as string[]);
return ids.map((id) => result.find((a) => a.id === id) || null);
}),
reviewsByBookLoader: new DataLoader(async (bookIds) => {
const allReviews = await db.reviews.findByBookIds(bookIds as string[]);
return bookIds.map((id) => allReviews.filter((r) => r.bookId === id));
}),
};
}
4. Embrace Discriminated Unions for Error Handling
Use TypeScript discriminated unions to model success and error states in mutations:
// Schema union type
const errorDefs = /* GraphQL */ `
type Mutation {
addReview(input: AddReviewInput!): AddReviewPayload!
}
input AddReviewInput {
bookId: ID!
reviewerName: String!
rating: Int!
comment: String!
}
type AddReviewSuccess {
review: Review!
}
type AddReviewError {
message: String!
code: String!
}
union AddReviewPayload = AddReviewSuccess | AddReviewError
`;
// Typed resolver return
type AddReviewResult =
| { __typename: "AddReviewSuccess"; review: Review }
| { __typename: "AddReviewError"; message: string; code: string };
function addReviewMutation(input: AddReviewInput): AddReviewResult {
const book = books.find((b) => b.id === input.bookId);
if (!book) {
return {
__typename: "AddReviewError",
message: "Book not found",
code: "NOT_FOUND",
};
}
// ... success logic
return {
__typename: "AddReviewSuccess",
review: newReview,
};
}
5. Leverage Middleware for Cross-Cutting Concerns
GraphQL Yoga supports a plugin system. Create typed plugins for logging, authentication, and performance monitoring:
import { Plugin } from "graphql-yoga";
interface LoggingPluginOptions {
logLevel: "debug" | "info" | "warn" | "error";
}
function createLoggingPlugin(options: LoggingPluginOptions): Plugin {
return {
onExecute: ({ executionArgs }) => {
console.log(`[${options.logLevel}] Executing: ${executionArgs.contextValue.requestId}`);
return {
onExecuteDone: ({ result }) => {
console.log(`[${options.logLevel}] Completed in ${result.duration}ms`);
},
};
},
};
}
6. Validate Input with Zod
Combine Yoga's argument types with Zod schemas for runtime validation that complements your compile-time types:
import { z } from "zod";
const AddReviewInputSchema = z.object({
bookId: z.string().uuid(),
reviewerName: z.string().min(2).max(100),
rating: z.number().int().min(1).max(5),
comment: z.string().min(10).max(1000),
});
// Use in resolver
addReview: (_parent, args, context) => {
const parsed = AddReviewInputSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Validation error: ${parsed.error.message}`);
}
// ... proceed with validated data
}
7. Structure Your Project for Scalability
Organize your codebase by domain rather than by technical layer:
src/
├── domains/
│ ├── books/
│ │ ├── books.schema.ts
│ │ ├── books.resolvers.ts
│ │ ├── books.data.ts
│ │ └── books.test.ts
│ ├── authors/
│ │ ├── authors.schema.ts
│ │ ├── authors.resolvers.ts
│ │ └── authors.data.ts
│ └── reviews/
│ ├── reviews.schema.ts
│ ├── reviews.resolvers.ts
│ └── reviews.data.ts
├── shared/
│ ├── context.ts
│ ├── plugins.ts
│ └── utils.ts
├── generated/
│ └── types.ts
└── index.ts
Testing Your Strongly Typed Yoga Server
GraphQL Yoga provides a built-in testing utility that respects your TypeScript types:
import { buildSchema } from "graphql-yoga";
import { typeDefs } from "../schema.js";
import { resolvers } from "../resolvers.js";
const schema = buildSchema({ typeDefs, resolvers });
// Execute queries against the schema without starting an HTTP server
async function testQuery() {
const result = await schema.execute({
document: /* GraphQL */ `
query GetBooks {
books {
title
author {
name
}
reviews {
rating
}
}
}
`,
contextValue: { requestId: "test-123", timestamp: new Date() },
});
// TypeScript now knows the shape of result.data
if (result.data) {
const firstBook = result.data.books[0];
console.log(`First book: ${firstBook.title} by ${firstBook.author.name}`);
}
}
Deployment Considerations
When deploying your GraphQL Yoga TypeScript application to production, keep these points in mind:
- Enable Error Masking — Set
maskedErrors: truein production to prevent leaking internal details through error messages. - Use a Reverse Proxy — Place Yoga behind nginx or a cloud load balancer for TLS termination and rate limiting.
- Implement Persisted Queries — Reduce attack surface and improve performance by only allowing pre-registered queries in production.
- Monitor Memory Usage — The in-memory examples above are for demonstration; use a proper database with connection pooling in production.
- Compile TypeScript Before Deployment — Run
tscas part of your CI/CD pipeline and deploy the compiled JavaScript.
Conclusion
GraphQL Yoga with TypeScript represents the modern gold standard for building GraphQL APIs. The combination delivers a developer experience where types flow seamlessly from your schema definition through your resolver logic, context, and all the way to your data layer. This eliminates entire categories of runtime errors, provides rich IDE autocompletion, and makes refactoring safe and predictable. By following the patterns outlined in this tutorial — explicit resolver typing, code generation with GraphQL Code Generator, typed context injection, DataLoader integration, and domain-driven project structure — you can build production-grade GraphQL servers that are maintainable, scalable, and a joy to work with. The investment in strong typing pays dividends throughout the entire lifecycle of your application, from initial development through ongoing maintenance and evolution.