+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 121 of 354

🔥 GraphQL with TypeScript: Type-Safe Queries

Master GraphQL with TypeScript for type-safe schema definitions, code generation, and enterprise-ready API development patterns 🚀

💎Advanced
22 min read

Prerequisites

  • Strong understanding of TypeScript generics and interfaces 📝
  • Experience with async/await and Promise patterns ⚡
  • Basic knowledge of GraphQL concepts and schema design 💻

What you'll learn

  • Master GraphQL schema development with TypeScript safety 🎯
  • Implement type-safe GraphQL clients with automatic code generation 🏗️
  • Build scalable GraphQL servers with resolvers and middleware 🐛
  • Create enterprise-ready GraphQL architectures with testing ✨

🎯 Introduction

Welcome to the future of API development with GraphQL and TypeScript! 🔥 If REST APIs are like ordering from a fixed menu, then GraphQL is like having a personal chef who creates exactly what you need, when you need it - with full type safety!

GraphQL revolutionizes how we think about APIs by allowing clients to request exactly the data they need, while TypeScript ensures every query, mutation, and subscription is type-safe from schema to client. Together, they create an incredibly powerful development experience that scales from simple apps to enterprise systems.

By the end of this tutorial, you’ll be a GraphQL + TypeScript expert, capable of building type-safe, performant, and maintainable GraphQL APIs that provide amazing developer experiences. Let’s dive into the graph! 🌐

📚 Understanding GraphQL with TypeScript

🤔 What Makes GraphQL + TypeScript Special?

GraphQL provides a query language and runtime for APIs, while TypeScript adds compile-time type safety. Together, they eliminate the guesswork from API development and ensure your schemas, resolvers, and clients are always in sync.

// 🌟 Basic GraphQL schema with TypeScript
import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql';

// 📦 Type definitions
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  posts: Post[];
}

interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  author?: User;
  createdAt: Date;
  updatedAt: Date;
}

// 🎯 GraphQL type definitions with TypeScript
const UserType = new GraphQLObjectType<User>({
  name: 'User',
  description: 'A user in the system',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The unique identifier for the user',
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The user\'s full name',
    },
    email: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The user\'s email address',
    },
    age: {
      type: GraphQLInt,
      description: 'The user\'s age in years',
    },
    posts: {
      type: new GraphQLList(PostType),
      description: 'Posts authored by this user',
      resolve: async (user: User, args, context) => {
        console.log(`🔍 Resolving posts for user: ${user.id}`);
        return context.dataSources.posts.getByAuthorId(user.id);
      },
    },
  }),
});

const PostType = new GraphQLObjectType<Post>({
  name: 'Post',
  description: 'A blog post',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The unique identifier for the post',
    },
    title: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The post title',
    },
    content: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The post content',
    },
    author: {
      type: UserType,
      description: 'The author of this post',
      resolve: async (post: Post, args, context) => {
        console.log(`👤 Resolving author for post: ${post.id}`);
        return context.dataSources.users.getById(post.authorId);
      },
    },
    createdAt: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'When the post was created',
      resolve: (post: Post) => post.createdAt.toISOString(),
    },
  }),
});

// 🚀 Root query type
const QueryType = new GraphQLObjectType({
  name: 'Query',
  description: 'The root query type',
  fields: {
    user: {
      type: UserType,
      description: 'Get a user by ID',
      args: {
        id: {
          type: new GraphQLNonNull(GraphQLString),
          description: 'The user ID',
        },
      },
      resolve: async (root, { id }, context) => {
        console.log(`🔍 Fetching user: ${id}`);
        return context.dataSources.users.getById(id);
      },
    },
    users: {
      type: new GraphQLList(UserType),
      description: 'Get all users',
      resolve: async (root, args, context) => {
        console.log('📋 Fetching all users');
        return context.dataSources.users.getAll();
      },
    },
    post: {
      type: PostType,
      description: 'Get a post by ID',
      args: {
        id: {
          type: new GraphQLNonNull(GraphQLString),
          description: 'The post ID',
        },
      },
      resolve: async (root, { id }, context) => {
        console.log(`📝 Fetching post: ${id}`);
        return context.dataSources.posts.getById(id);
      },
    },
  },
});

// 🎨 Create the schema
export const schema = new GraphQLSchema({
  query: QueryType,
  description: 'A simple blog schema with users and posts',
});

💡 Key Advantages

  • 🎯 Type Safety: Full type checking from schema to client
  • 📊 Single Endpoint: One URL for all data operations
  • ⚡ Efficient Queries: Fetch exactly what you need
  • 🔄 Real-time: Built-in subscriptions for live data
  • 📱 Developer Experience: Introspection and tooling
  • 🔧 Code Generation: Automatic TypeScript types from schemas
// 🎨 Advanced GraphQL setup with TypeScript
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { Container } from 'typedi';
import express from 'express';

// 📦 Data source interfaces
interface DataSources {
  users: UserDataSource;
  posts: PostDataSource;
}

interface Context {
  user?: User;
  dataSources: DataSources;
  req: express.Request;
  res: express.Response;
}

// 🏗️ Data source implementations
class UserDataSource {
  private users: Map<string, User> = new Map();

  constructor() {
    // 🎭 Mock data for demonstration
    this.users.set('1', {
      id: '1',
      name: 'Alice Johnson',
      email: '[email protected]',
      age: 30,
      posts: [],
    });

    this.users.set('2', {
      id: '2',
      name: 'Bob Smith',
      email: '[email protected]',
      age: 25,
      posts: [],
    });
  }

  async getById(id: string): Promise<User | null> {
    console.log(`👤 UserDataSource.getById(${id})`);
    return this.users.get(id) || null;
  }

  async getAll(): Promise<User[]> {
    console.log('👥 UserDataSource.getAll()');
    return Array.from(this.users.values());
  }

  async getByIds(ids: string[]): Promise<User[]> {
    console.log(`👥 UserDataSource.getByIds([${ids.join(', ')}])`);
    return ids.map(id => this.users.get(id)).filter(Boolean) as User[];
  }

  async create(userData: Omit<User, 'id' | 'posts'>): Promise<User> {
    const id = (this.users.size + 1).toString();
    const user: User = {
      ...userData,
      id,
      posts: [],
    };
    
    this.users.set(id, user);
    console.log(`✨ Created user: ${user.id}`);
    return user;
  }

  async update(id: string, updates: Partial<User>): Promise<User | null> {
    const user = this.users.get(id);
    if (!user) return null;

    const updatedUser = { ...user, ...updates };
    this.users.set(id, updatedUser);
    console.log(`🔄 Updated user: ${id}`);
    return updatedUser;
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.users.delete(id);
    if (deleted) {
      console.log(`🗑️ Deleted user: ${id}`);
    }
    return deleted;
  }
}

class PostDataSource {
  private posts: Map<string, Post> = new Map();

  constructor() {
    // 🎭 Mock data for demonstration
    this.posts.set('1', {
      id: '1',
      title: 'Getting Started with GraphQL',
      content: 'GraphQL is a query language for APIs...',
      authorId: '1',
      createdAt: new Date('2023-01-01'),
      updatedAt: new Date('2023-01-01'),
    });

    this.posts.set('2', {
      id: '2',
      title: 'TypeScript Best Practices',
      content: 'TypeScript provides type safety for JavaScript...',
      authorId: '2',
      createdAt: new Date('2023-01-02'),
      updatedAt: new Date('2023-01-02'),
    });
  }

  async getById(id: string): Promise<Post | null> {
    console.log(`📝 PostDataSource.getById(${id})`);
    return this.posts.get(id) || null;
  }

  async getAll(): Promise<Post[]> {
    console.log('📚 PostDataSource.getAll()');
    return Array.from(this.posts.values());
  }

  async getByAuthorId(authorId: string): Promise<Post[]> {
    console.log(`📝 PostDataSource.getByAuthorId(${authorId})`);
    return Array.from(this.posts.values()).filter(post => post.authorId === authorId);
  }

  async create(postData: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>): Promise<Post> {
    const id = (this.posts.size + 1).toString();
    const now = new Date();
    const post: Post = {
      ...postData,
      id,
      createdAt: now,
      updatedAt: now,
    };
    
    this.posts.set(id, post);
    console.log(`✨ Created post: ${post.id}`);
    return post;
  }

  async update(id: string, updates: Partial<Post>): Promise<Post | null> {
    const post = this.posts.get(id);
    if (!post) return null;

    const updatedPost = {
      ...post,
      ...updates,
      updatedAt: new Date(),
    };
    
    this.posts.set(id, updatedPost);
    console.log(`🔄 Updated post: ${id}`);
    return updatedPost;
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.posts.delete(id);
    if (deleted) {
      console.log(`🗑️ Deleted post: ${id}`);
    }
    return deleted;
  }
}

// 🚀 Server setup with context
const createServer = async (): Promise<ApolloServer> => {
  const server = new ApolloServer({
    schema,
    context: ({ req, res }): Context => {
      return {
        user: req.user, // From authentication middleware
        dataSources: {
          users: new UserDataSource(),
          posts: new PostDataSource(),
        },
        req,
        res,
      };
    },
    formatError: (error) => {
      console.error('🚨 GraphQL Error:', error);
      return {
        message: error.message,
        code: error.extensions?.code,
        path: error.path,
      };
    },
    formatResponse: (response) => {
      console.log('📊 GraphQL Response:', {
        data: !!response.data,
        errors: response.errors?.length || 0,
      });
      return response;
    },
  });

  return server;
};

// 🎮 Usage example
const startServer = async (): Promise<void> => {
  try {
    const app = express();
    const server = await createServer();
    
    await server.start();
    server.applyMiddleware({ app, path: '/graphql' });
    
    const PORT = process.env.PORT || 4000;
    app.listen(PORT, () => {
      console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
      console.log(`🔍 GraphQL Playground available at http://localhost:${PORT}${server.graphqlPath}`);
    });
  } catch (error) {
    console.error('💥 Failed to start server:', error);
  }
};

🛠️ Type-GraphQL: Schema-First Development

🎯 Decorators and Class-Based Schemas

Type-GraphQL allows you to create GraphQL schemas using TypeScript classes and decorators, providing excellent type safety and developer experience:

// 🎨 Type-GraphQL schema definitions
import { ObjectType, Field, ID, Int, Resolver, Query, Mutation, Arg, Ctx, InputType } from 'type-graphql';
import { IsEmail, MinLength, MaxLength } from 'class-validator';

// 📦 Entity definitions
@ObjectType({ description: 'A user in the system' })
export class User {
  @Field(() => ID)
  id: string;

  @Field({ description: 'The user\'s full name' })
  name: string;

  @Field({ description: 'The user\'s email address' })
  email: string;

  @Field(() => Int, { nullable: true, description: 'The user\'s age in years' })
  age?: number;

  @Field(() => [Post], { description: 'Posts authored by this user' })
  posts: Post[];

  @Field({ description: 'When the user was created' })
  createdAt: Date;

  @Field({ description: 'When the user was last updated' })
  updatedAt: Date;
}

@ObjectType({ description: 'A blog post' })
export class Post {
  @Field(() => ID)
  id: string;

  @Field({ description: 'The post title' })
  title: string;

  @Field({ description: 'The post content' })
  content: string;

  @Field({ description: 'The post excerpt' })
  excerpt: string;

  @Field(() => User, { description: 'The author of this post' })
  author: User;

  @Field(() => [String], { description: 'Post tags' })
  tags: string[];

  @Field({ description: 'Whether the post is published' })
  published: boolean;

  @Field({ description: 'When the post was created' })
  createdAt: Date;

  @Field({ description: 'When the post was last updated' })
  updatedAt: Date;
}

// 🎯 Input types for mutations
@InputType({ description: 'Input for creating a new user' })
export class CreateUserInput {
  @Field({ description: 'The user\'s full name' })
  @MinLength(2, { message: 'Name must be at least 2 characters long' })
  @MaxLength(50, { message: 'Name must not exceed 50 characters' })
  name: string;

  @Field({ description: 'The user\'s email address' })
  @IsEmail({}, { message: 'Must be a valid email address' })
  email: string;

  @Field(() => Int, { nullable: true, description: 'The user\'s age in years' })
  age?: number;
}

@InputType({ description: 'Input for updating a user' })
export class UpdateUserInput {
  @Field({ nullable: true, description: 'The user\'s full name' })
  @MinLength(2, { message: 'Name must be at least 2 characters long' })
  @MaxLength(50, { message: 'Name must not exceed 50 characters' })
  name?: string;

  @Field({ nullable: true, description: 'The user\'s email address' })
  @IsEmail({}, { message: 'Must be a valid email address' })
  email?: string;

  @Field(() => Int, { nullable: true, description: 'The user\'s age in years' })
  age?: number;
}

@InputType({ description: 'Input for creating a new post' })
export class CreatePostInput {
  @Field({ description: 'The post title' })
  @MinLength(5, { message: 'Title must be at least 5 characters long' })
  @MaxLength(100, { message: 'Title must not exceed 100 characters' })
  title: string;

  @Field({ description: 'The post content' })
  @MinLength(10, { message: 'Content must be at least 10 characters long' })
  content: string;

  @Field(() => [String], { defaultValue: [], description: 'Post tags' })
  tags: string[];

  @Field({ defaultValue: false, description: 'Whether the post is published' })
  published: boolean;
}

// 🏗️ Service classes
export class UserService {
  private users: Map<string, User> = new Map();

  async findById(id: string): Promise<User | null> {
    console.log(`🔍 UserService.findById(${id})`);
    return this.users.get(id) || null;
  }

  async findAll(): Promise<User[]> {
    console.log('👥 UserService.findAll()');
    return Array.from(this.users.values());
  }

  async create(input: CreateUserInput): Promise<User> {
    const id = (this.users.size + 1).toString();
    const now = new Date();
    
    const user: User = {
      id,
      ...input,
      posts: [],
      createdAt: now,
      updatedAt: now,
    };

    this.users.set(id, user);
    console.log(`✨ Created user: ${user.id}`);
    return user;
  }

  async update(id: string, input: UpdateUserInput): Promise<User | null> {
    const user = this.users.get(id);
    if (!user) return null;

    const updatedUser: User = {
      ...user,
      ...input,
      updatedAt: new Date(),
    };

    this.users.set(id, updatedUser);
    console.log(`🔄 Updated user: ${id}`);
    return updatedUser;
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.users.delete(id);
    if (deleted) {
      console.log(`🗑️ Deleted user: ${id}`);
    }
    return deleted;
  }
}

export class PostService {
  private posts: Map<string, Post> = new Map();

  async findById(id: string): Promise<Post | null> {
    console.log(`📝 PostService.findById(${id})`);
    return this.posts.get(id) || null;
  }

  async findAll(): Promise<Post[]> {
    console.log('📚 PostService.findAll()');
    return Array.from(this.posts.values());
  }

  async findByAuthorId(authorId: string): Promise<Post[]> {
    console.log(`📝 PostService.findByAuthorId(${authorId})`);
    return Array.from(this.posts.values()).filter(post => 
      (post.author as any).id === authorId
    );
  }

  async create(input: CreatePostInput, authorId: string): Promise<Post> {
    const id = (this.posts.size + 1).toString();
    const now = new Date();
    
    // Generate excerpt from content
    const excerpt = input.content.substring(0, 150) + 
      (input.content.length > 150 ? '...' : '');

    const post: Post = {
      id,
      ...input,
      excerpt,
      author: { id: authorId } as User, // Will be resolved
      createdAt: now,
      updatedAt: now,
    };

    this.posts.set(id, post);
    console.log(`✨ Created post: ${post.id}`);
    return post;
  }

  async update(id: string, input: Partial<CreatePostInput>): Promise<Post | null> {
    const post = this.posts.get(id);
    if (!post) return null;

    const updatedPost: Post = {
      ...post,
      ...input,
      updatedAt: new Date(),
    };

    // Update excerpt if content changed
    if (input.content) {
      updatedPost.excerpt = input.content.substring(0, 150) + 
        (input.content.length > 150 ? '...' : '');
    }

    this.posts.set(id, updatedPost);
    console.log(`🔄 Updated post: ${id}`);
    return updatedPost;
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.posts.delete(id);
    if (deleted) {
      console.log(`🗑️ Deleted post: ${id}`);
    }
    return deleted;
  }
}

// 🎯 Resolvers with dependency injection
@Resolver(() => User)
export class UserResolver {
  constructor(
    private userService: UserService,
    private postService: PostService
  ) {}

  @Query(() => User, { nullable: true, description: 'Get a user by ID' })
  async user(@Arg('id', () => ID) id: string): Promise<User | null> {
    return this.userService.findById(id);
  }

  @Query(() => [User], { description: 'Get all users' })
  async users(): Promise<User[]> {
    return this.userService.findAll();
  }

  @Mutation(() => User, { description: 'Create a new user' })
  async createUser(@Arg('input') input: CreateUserInput): Promise<User> {
    return this.userService.create(input);
  }

  @Mutation(() => User, { nullable: true, description: 'Update a user' })
  async updateUser(
    @Arg('id', () => ID) id: string,
    @Arg('input') input: UpdateUserInput
  ): Promise<User | null> {
    return this.userService.update(id, input);
  }

  @Mutation(() => Boolean, { description: 'Delete a user' })
  async deleteUser(@Arg('id', () => ID) id: string): Promise<boolean> {
    return this.userService.delete(id);
  }

  // 🔗 Field resolver for posts
  @Field(() => [Post])
  async posts(@Arg('root') user: User): Promise<Post[]> {
    return this.postService.findByAuthorId(user.id);
  }
}

@Resolver(() => Post)
export class PostResolver {
  constructor(
    private postService: PostService,
    private userService: UserService
  ) {}

  @Query(() => Post, { nullable: true, description: 'Get a post by ID' })
  async post(@Arg('id', () => ID) id: string): Promise<Post | null> {
    return this.postService.findById(id);
  }

  @Query(() => [Post], { description: 'Get all posts' })
  async posts(): Promise<Post[]> {
    return this.postService.findAll();
  }

  @Mutation(() => Post, { description: 'Create a new post' })
  async createPost(
    @Arg('input') input: CreatePostInput,
    @Arg('authorId', () => ID) authorId: string
  ): Promise<Post> {
    return this.postService.create(input, authorId);
  }

  @Mutation(() => Post, { nullable: true, description: 'Update a post' })
  async updatePost(
    @Arg('id', () => ID) id: string,
    @Arg('input') input: Partial<CreatePostInput>
  ): Promise<Post | null> {
    return this.postService.update(id, input);
  }

  @Mutation(() => Boolean, { description: 'Delete a post' })
  async deletePost(@Arg('id', () => ID) id: string): Promise<boolean> {
    return this.postService.delete(id);
  }

  // 🔗 Field resolver for author
  @Field(() => User)
  async author(@Arg('root') post: Post): Promise<User | null> {
    return this.userService.findById((post.author as any).id);
  }
}

// 🏗️ Schema building
const buildTypeGraphQLSchema = async (): Promise<GraphQLSchema> => {
  return buildSchema({
    resolvers: [UserResolver, PostResolver],
    container: Container,
    validate: true,
    emitSchemaFile: true, // Generate schema.gql file
  });
};

📊 Advanced Query Patterns

Implement complex query patterns with DataLoader for efficient data fetching:

// 🔄 DataLoader for efficient data fetching
import DataLoader from 'dataloader';

// 📦 DataLoader implementations
export class UserDataLoader {
  private userService: UserService;
  private loader: DataLoader<string, User | null>;

  constructor(userService: UserService) {
    this.userService = userService;
    this.loader = new DataLoader(this.batchLoadUsers.bind(this));
  }

  private async batchLoadUsers(ids: readonly string[]): Promise<(User | null)[]> {
    console.log(`🔄 Batch loading users: [${ids.join(', ')}]`);
    
    // Simulate batch database query
    const users = await Promise.all(
      ids.map(id => this.userService.findById(id))
    );

    return users;
  }

  async load(id: string): Promise<User | null> {
    return this.loader.load(id);
  }

  async loadMany(ids: string[]): Promise<(User | null)[]> {
    return this.loader.loadMany(ids);
  }

  clear(id: string): void {
    this.loader.clear(id);
  }

  clearAll(): void {
    this.loader.clearAll();
  }
}

export class PostDataLoader {
  private postService: PostService;
  private loader: DataLoader<string, Post | null>;
  private byAuthorLoader: DataLoader<string, Post[]>;

  constructor(postService: PostService) {
    this.postService = postService;
    this.loader = new DataLoader(this.batchLoadPosts.bind(this));
    this.byAuthorLoader = new DataLoader(this.batchLoadPostsByAuthor.bind(this));
  }

  private async batchLoadPosts(ids: readonly string[]): Promise<(Post | null)[]> {
    console.log(`🔄 Batch loading posts: [${ids.join(', ')}]`);
    
    const posts = await Promise.all(
      ids.map(id => this.postService.findById(id))
    );

    return posts;
  }

  private async batchLoadPostsByAuthor(authorIds: readonly string[]): Promise<Post[][]> {
    console.log(`🔄 Batch loading posts by authors: [${authorIds.join(', ')}]`);
    
    const postsByAuthor = await Promise.all(
      authorIds.map(authorId => this.postService.findByAuthorId(authorId))
    );

    return postsByAuthor;
  }

  async load(id: string): Promise<Post | null> {
    return this.loader.load(id);
  }

  async loadByAuthor(authorId: string): Promise<Post[]> {
    return this.byAuthorLoader.load(authorId);
  }
}

// 🎯 Enhanced context with DataLoaders
interface EnhancedContext extends Context {
  dataSources: DataSources;
  loaders: {
    users: UserDataLoader;
    posts: PostDataLoader;
  };
}

// 🏗️ Enhanced resolver with DataLoaders
@Resolver(() => User)
export class EnhancedUserResolver {
  @Query(() => User, { nullable: true })
  async user(
    @Arg('id', () => ID) id: string,
    @Ctx() context: EnhancedContext
  ): Promise<User | null> {
    // Use DataLoader for efficient batching
    return context.loaders.users.load(id);
  }

  @Query(() => [User])
  async users(@Ctx() context: EnhancedContext): Promise<User[]> {
    return context.dataSources.users.getAll();
  }

  // 🔗 Efficient field resolver using DataLoader
  @Field(() => [Post])
  async posts(
    @Arg('root') user: User,
    @Ctx() context: EnhancedContext
  ): Promise<Post[]> {
    return context.loaders.posts.loadByAuthor(user.id);
  }
}

@Resolver(() => Post)
export class EnhancedPostResolver {
  @Query(() => Post, { nullable: true })
  async post(
    @Arg('id', () => ID) id: string,
    @Ctx() context: EnhancedContext
  ): Promise<Post | null> {
    return context.loaders.posts.load(id);
  }

  // 🔗 Efficient field resolver using DataLoader
  @Field(() => User)
  async author(
    @Arg('root') post: Post,
    @Ctx() context: EnhancedContext
  ): Promise<User | null> {
    return context.loaders.users.load((post.author as any).id);
  }
}

// 🚀 Enhanced server setup
const createEnhancedServer = async (): Promise<ApolloServer> => {
  const schema = await buildTypeGraphQLSchema();
  
  const server = new ApolloServer({
    schema,
    context: ({ req, res }): EnhancedContext => {
      const userService = new UserService();
      const postService = new PostService();

      return {
        user: req.user,
        dataSources: {
          users: new UserDataSource(),
          posts: new PostDataSource(),
        },
        loaders: {
          users: new UserDataLoader(userService),
          posts: new PostDataLoader(postService),
        },
        req,
        res,
      };
    },
    plugins: [
      // 📊 Query complexity analysis
      {
        requestDidStart() {
          return {
            didResolveOperation({ request, document }) {
              console.log(`📊 Operation: ${request.operationName}`);
              console.log(`📝 Query: ${request.query?.substring(0, 100)}...`);
            },
            willSendResponse({ response }) {
              console.log(`📈 Response time: ${Date.now()}ms`);
            },
          };
        },
      },
    ],
  });

  return server;
};

🎯 Client-Side TypeScript Integration

🔥 Apollo Client with Code Generation

Generate TypeScript types from your GraphQL schema for complete type safety:

// 🛠️ Setup for GraphQL code generation
// First, install dependencies:
// npm install @apollo/client graphql
// npm install -D @graphql-codegen/cli @graphql-codegen/typescript
// npm install -D @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

// codegen.yml configuration file:
/*
overwrite: true
schema: "http://localhost:4000/graphql"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      withHooks: true
      withComponent: false
      withHOC: false
*/

// 🎯 GraphQL operations (save as src/operations/users.graphql)
/*
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    age
    posts {
      id
      title
      excerpt
      createdAt
    }
  }
}

query GetUsers {
  users {
    id
    name
    email
    age
  }
}

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
    age
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
    age
  }
}

query GetPost($id: ID!) {
  post(id: $id) {
    id
    title
    content
    excerpt
    tags
    published
    author {
      id
      name
      email
    }
    createdAt
    updatedAt
  }
}

query GetPosts {
  posts {
    id
    title
    excerpt
    tags
    published
    author {
      id
      name
    }
    createdAt
  }
}

mutation CreatePost($input: CreatePostInput!, $authorId: ID!) {
  createPost(input: $input, authorId: $authorId) {
    id
    title
    content
    excerpt
    tags
    published
    author {
      id
      name
    }
    createdAt
  }
}
*/

// 🏗️ Apollo Client setup with TypeScript
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

// 📦 Generated types (after running codegen)
import {
  GetUserQuery,
  GetUserQueryVariables,
  GetUsersQuery,
  CreateUserMutation,
  CreateUserMutationVariables,
  UpdateUserMutation,
  UpdateUserMutationVariables,
  GetPostQuery,
  GetPostQueryVariables,
  GetPostsQuery,
  CreatePostMutation,
  CreatePostMutationVariables,
  // Generated hooks
  useGetUserQuery,
  useGetUsersQuery,
  useCreateUserMutation,
  useUpdateUserMutation,
  useGetPostQuery,
  useGetPostsQuery,
  useCreatePostMutation,
} from '../generated/graphql';

// 🔗 HTTP link
const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql',
});

// 🔑 Auth link
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('authToken');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

// 🚨 Error link
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        `🚨 GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}`,
        extensions
      );
    });
  }

  if (networkError) {
    console.error(`🌐 Network error: ${networkError}`);
    
    // Handle token expiration
    if ('statusCode' in networkError && networkError.statusCode === 401) {
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
  }
});

// 🏗️ Apollo Client instance
export const apolloClient = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return incoming;
            },
          },
        },
      },
      Post: {
        fields: {
          author: {
            merge(existing, incoming) {
              return incoming;
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all',
    },
    query: {
      errorPolicy: 'all',
    },
  },
});

// 🎯 Type-safe React components
import React, { useState } from 'react';
import { useMutation, useQuery } from '@apollo/client';

// 👤 User Profile Component
interface UserProfileProps {
  userId: string;
}

export const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
  // 🔍 Type-safe query with generated hook
  const { data, loading, error, refetch } = useGetUserQuery({
    variables: { id: userId },
    errorPolicy: 'all',
  });

  // 🔄 Type-safe mutation with generated hook
  const [updateUser, { loading: updating }] = useUpdateUserMutation({
    onCompleted: (data) => {
      console.log('✅ User updated:', data.updateUser);
    },
    onError: (error) => {
      console.error('❌ Update failed:', error);
    },
  });

  const [editMode, setEditMode] = useState(false);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: 0,
  });

  React.useEffect(() => {
    if (data?.user) {
      setFormData({
        name: data.user.name,
        email: data.user.email,
        age: data.user.age || 0,
      });
    }
  }, [data]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      await updateUser({
        variables: {
          id: userId,
          input: {
            name: formData.name,
            email: formData.email,
            age: formData.age || undefined,
          },
        },
        // 🔄 Update cache optimistically
        optimisticResponse: {
          updateUser: {
            __typename: 'User',
            id: userId,
            name: formData.name,
            email: formData.email,
            age: formData.age,
            posts: data?.user?.posts || [],
            createdAt: data?.user?.createdAt || new Date().toISOString(),
            updatedAt: new Date().toISOString(),
          },
        },
      });
      
      setEditMode(false);
    } catch (error) {
      console.error('💥 Failed to update user:', error);
    }
  };

  if (loading) return <div>⏳ Loading user...</div>;
  if (error && !data) return <div>❌ Error: {error.message}</div>;
  if (!data?.user) return <div>👤 User not found</div>;

  const { user } = data;

  return (
    <div className="user-profile">
      <h2>👤 User Profile</h2>
      
      {editMode ? (
        <form onSubmit={handleSubmit}>
          <div>
            <label htmlFor="name">Name:</label>
            <input
              id="name"
              type="text"
              value={formData.name}
              onChange={(e) => setFormData({ ...formData, name: e.target.value })}
              required
            />
          </div>
          
          <div>
            <label htmlFor="email">Email:</label>
            <input
              id="email"
              type="email"
              value={formData.email}
              onChange={(e) => setFormData({ ...formData, email: e.target.value })}
              required
            />
          </div>
          
          <div>
            <label htmlFor="age">Age:</label>
            <input
              id="age"
              type="number"
              value={formData.age}
              onChange={(e) => setFormData({ ...formData, age: parseInt(e.target.value) })}
            />
          </div>
          
          <div>
            <button type="submit" disabled={updating}>
              {updating ? '⏳ Saving...' : '💾 Save'}
            </button>
            <button type="button" onClick={() => setEditMode(false)}>
Cancel
            </button>
          </div>
        </form>
      ) : (
        <div>
          <p><strong>Name:</strong> {user.name}</p>
          <p><strong>Email:</strong> {user.email}</p>
          {user.age && <p><strong>Age:</strong> {user.age}</p>}
          
          <h3>📝 Posts ({user.posts?.length || 0})</h3>
          <ul>
            {user.posts?.map(post => (
              <li key={post.id}>
                <h4>{post.title}</h4>
                <p>{post.excerpt}</p>
                <small>📅 {new Date(post.createdAt).toLocaleDateString()}</small>
              </li>
            ))}
          </ul>
          
          <div>
            <button onClick={() => setEditMode(true)}>
              ✏️ Edit Profile
            </button>
            <button onClick={() => refetch()}>
              🔄 Refresh
            </button>
          </div>
        </div>
      )}
    </div>
  );
};

// 📝 Posts List Component
export const PostsList: React.FC = () => {
  const { data, loading, error } = useGetPostsQuery({
    errorPolicy: 'all',
  });

  const [createPost] = useCreatePostMutation({
    // 🔄 Update cache after mutation
    update: (cache, { data }) => {
      if (data?.createPost) {
        const existingPosts = cache.readQuery<GetPostsQuery>({
          query: GET_POSTS,
        });

        if (existingPosts) {
          cache.writeQuery<GetPostsQuery>({
            query: GET_POSTS,
            data: {
              posts: [data.createPost, ...existingPosts.posts],
            },
          });
        }
      }
    },
  });

  if (loading) return <div>⏳ Loading posts...</div>;
  if (error && !data) return <div>❌ Error: {error.message}</div>;

  return (
    <div className="posts-list">
      <h2>📚 Blog Posts</h2>
      
      {data?.posts?.map(post => (
        <article key={post.id} className="post-card">
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
          <div className="post-meta">
            <span>👤 By {post.author.name}</span>
            <span>📅 {new Date(post.createdAt).toLocaleDateString()}</span>
            {post.tags.length > 0 && (
              <span>🏷️ {post.tags.join(', ')}</span>
            )}
          </div>
        </article>
      ))}
    </div>
  );
};

// 🎮 Usage example
export const App: React.FC = () => {
  return (
    <div className="app">
      <h1>🔥 GraphQL + TypeScript Blog</h1>
      <UserProfile userId="1" />
      <PostsList />
    </div>
  );
};

🧪 Testing GraphQL with TypeScript

🎭 Comprehensive Testing Strategies

Test your GraphQL schemas, resolvers, and client integration:

// 🧪 Testing utilities and setup
import { buildSchema } from 'type-graphql';
import { graphql, GraphQLSchema } from 'graphql';
import { Container } from 'typedi';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// 🎭 Mock services for testing
class MockUserService {
  private users: Map<string, User> = new Map([
    ['1', {
      id: '1',
      name: 'Test User',
      email: '[email protected]',
      age: 30,
      posts: [],
      createdAt: new Date('2023-01-01'),
      updatedAt: new Date('2023-01-01'),
    }],
  ]);

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async findAll(): Promise<User[]> {
    return Array.from(this.users.values());
  }

  async create(input: CreateUserInput): Promise<User> {
    const id = (this.users.size + 1).toString();
    const now = new Date();
    
    const user: User = {
      id,
      ...input,
      posts: [],
      createdAt: now,
      updatedAt: now,
    };

    this.users.set(id, user);
    return user;
  }

  async update(id: string, input: UpdateUserInput): Promise<User | null> {
    const user = this.users.get(id);
    if (!user) return null;

    const updatedUser = { ...user, ...input, updatedAt: new Date() };
    this.users.set(id, updatedUser);
    return updatedUser;
  }

  async delete(id: string): Promise<boolean> {
    return this.users.delete(id);
  }
}

class MockPostService {
  private posts: Map<string, Post> = new Map([
    ['1', {
      id: '1',
      title: 'Test Post',
      content: 'This is a test post content.',
      excerpt: 'This is a test post...',
      author: { id: '1' } as User,
      tags: ['test'],
      published: true,
      createdAt: new Date('2023-01-01'),
      updatedAt: new Date('2023-01-01'),
    }],
  ]);

  async findById(id: string): Promise<Post | null> {
    return this.posts.get(id) || null;
  }

  async findAll(): Promise<Post[]> {
    return Array.from(this.posts.values());
  }

  async findByAuthorId(authorId: string): Promise<Post[]> {
    return Array.from(this.posts.values()).filter(post => 
      (post.author as any).id === authorId
    );
  }

  async create(input: CreatePostInput, authorId: string): Promise<Post> {
    const id = (this.posts.size + 1).toString();
    const now = new Date();
    
    const post: Post = {
      id,
      ...input,
      excerpt: input.content.substring(0, 50) + '...',
      author: { id: authorId } as User,
      createdAt: now,
      updatedAt: now,
    };

    this.posts.set(id, post);
    return post;
  }

  async update(id: string, input: Partial<CreatePostInput>): Promise<Post | null> {
    const post = this.posts.get(id);
    if (!post) return null;

    const updatedPost = { ...post, ...input, updatedAt: new Date() };
    this.posts.set(id, updatedPost);
    return updatedPost;
  }

  async delete(id: string): Promise<boolean> {
    return this.posts.delete(id);
  }
}

// 🏗️ Test schema builder
const createTestSchema = async (): Promise<GraphQLSchema> => {
  // Override services with mocks
  Container.set(UserService, new MockUserService());
  Container.set(PostService, new MockPostService());

  return buildSchema({
    resolvers: [UserResolver, PostResolver],
    container: Container,
    validate: false, // Skip validation in tests
  });
};

// 🧪 Schema-level tests
describe('GraphQL Schema Tests', () => {
  let schema: GraphQLSchema;

  beforeAll(async () => {
    schema = await createTestSchema();
  });

  describe('User Queries', () => {
    it('should fetch user by ID', async () => {
      const query = `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            email
            age
          }
        }
      `;

      const result = await graphql({
        schema,
        source: query,
        variableValues: { id: '1' },
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.user).toEqual({
        id: '1',
        name: 'Test User',
        email: '[email protected]',
        age: 30,
      });
    });

    it('should return null for non-existent user', async () => {
      const query = `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
          }
        }
      `;

      const result = await graphql({
        schema,
        source: query,
        variableValues: { id: '999' },
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.user).toBeNull();
    });

    it('should fetch all users', async () => {
      const query = `
        query GetUsers {
          users {
            id
            name
            email
          }
        }
      `;

      const result = await graphql({
        schema,
        source: query,
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.users).toHaveLength(1);
      expect(result.data?.users[0]).toMatchObject({
        id: '1',
        name: 'Test User',
        email: '[email protected]',
      });
    });
  });

  describe('User Mutations', () => {
    it('should create a new user', async () => {
      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            name
            email
            age
          }
        }
      `;

      const input = {
        name: 'New User',
        email: '[email protected]',
        age: 25,
      };

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: { input },
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.createUser).toMatchObject({
        name: 'New User',
        email: '[email protected]',
        age: 25,
      });
      expect(result.data?.createUser.id).toBeTruthy();
    });

    it('should update an existing user', async () => {
      const mutation = `
        mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
          updateUser(id: $id, input: $input) {
            id
            name
            email
            age
          }
        }
      `;

      const input = {
        name: 'Updated User',
        age: 35,
      };

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: { id: '1', input },
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.updateUser).toMatchObject({
        id: '1',
        name: 'Updated User',
        email: '[email protected]', // Unchanged
        age: 35,
      });
    });

    it('should delete a user', async () => {
      // First create a user to delete
      const createMutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
          }
        }
      `;

      const createResult = await graphql({
        schema,
        source: createMutation,
        variableValues: {
          input: {
            name: 'To Delete',
            email: '[email protected]',
          },
        },
      });

      const userId = createResult.data?.createUser.id;

      // Now delete the user
      const deleteMutation = `
        mutation DeleteUser($id: ID!) {
          deleteUser(id: $id)
        }
      `;

      const deleteResult = await graphql({
        schema,
        source: deleteMutation,
        variableValues: { id: userId },
      });

      expect(deleteResult.errors).toBeUndefined();
      expect(deleteResult.data?.deleteUser).toBe(true);
    });
  });

  describe('Post Queries', () => {
    it('should fetch post with author', async () => {
      const query = `
        query GetPost($id: ID!) {
          post(id: $id) {
            id
            title
            content
            author {
              id
              name
              email
            }
          }
        }
      `;

      const result = await graphql({
        schema,
        source: query,
        variableValues: { id: '1' },
      });

      expect(result.errors).toBeUndefined();
      expect(result.data?.post).toMatchObject({
        id: '1',
        title: 'Test Post',
        content: 'This is a test post content.',
        author: {
          id: '1',
          name: 'Test User',
          email: '[email protected]',
        },
      });
    });
  });

  describe('Validation Tests', () => {
    it('should validate required fields', async () => {
      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
          }
        }
      `;

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: {
          input: {
            // Missing required name field
            email: '[email protected]',
          },
        },
      });

      expect(result.errors).toBeDefined();
      expect(result.errors?.[0].message).toContain('name');
    });

    it('should validate email format', async () => {
      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
          }
        }
      `;

      const result = await graphql({
        schema,
        source: mutation,
        variableValues: {
          input: {
            name: 'Test User',
            email: 'invalid-email', // Invalid email format
          },
        },
      });

      expect(result.errors).toBeDefined();
      expect(result.errors?.[0].message).toContain('email');
    });
  });
});

// 🎭 Client component tests
describe('GraphQL Client Components', () => {
  const mocks = [
    {
      request: {
        query: GET_USER,
        variables: { id: '1' },
      },
      result: {
        data: {
          user: {
            id: '1',
            name: 'Test User',
            email: '[email protected]',
            age: 30,
            posts: [
              {
                id: '1',
                title: 'Test Post',
                excerpt: 'Test excerpt...',
                createdAt: '2023-01-01T00:00:00Z',
              },
            ],
            createdAt: '2023-01-01T00:00:00Z',
            updatedAt: '2023-01-01T00:00:00Z',
          },
        },
      },
    },
    {
      request: {
        query: UPDATE_USER,
        variables: {
          id: '1',
          input: {
            name: 'Updated Name',
            email: '[email protected]',
          },
        },
      },
      result: {
        data: {
          updateUser: {
            id: '1',
            name: 'Updated Name',
            email: '[email protected]',
            age: 30,
            posts: [],
            createdAt: '2023-01-01T00:00:00Z',
            updatedAt: '2023-01-01T00:00:00Z',
          },
        },
      },
    },
  ];

  it('should render user profile', async () => {
    render(
      <MockedProvider mocks={mocks} addTypename={false}>
        <UserProfile userId="1" />
      </MockedProvider>
    );

    // Loading state
    expect(screen.getByText('⏳ Loading user...')).toBeInTheDocument();

    // Wait for data to load
    await waitFor(() => {
      expect(screen.getByText('👤 User Profile')).toBeInTheDocument();
      expect(screen.getByText('Test User')).toBeInTheDocument();
      expect(screen.getByText('[email protected]')).toBeInTheDocument();
    });

    // Check posts section
    expect(screen.getByText('📝 Posts (1)')).toBeInTheDocument();
    expect(screen.getByText('Test Post')).toBeInTheDocument();
  });

  it('should handle user profile editing', async () => {
    const user = userEvent.setup();

    render(
      <MockedProvider mocks={mocks} addTypename={false}>
        <UserProfile userId="1" />
      </MockedProvider>
    );

    // Wait for component to load
    await waitFor(() => {
      expect(screen.getByText('Test User')).toBeInTheDocument();
    });

    // Enter edit mode
    const editButton = screen.getByText('✏️ Edit Profile');
    await user.click(editButton);

    // Form should be visible
    expect(screen.getByDisplayValue('Test User')).toBeInTheDocument();
    expect(screen.getByDisplayValue('[email protected]')).toBeInTheDocument();

    // Update name
    const nameInput = screen.getByDisplayValue('Test User');
    await user.clear(nameInput);
    await user.type(nameInput, 'Updated Name');

    // Submit form
    const saveButton = screen.getByText('💾 Save');
    await user.click(saveButton);

    // Should exit edit mode
    await waitFor(() => {
      expect(screen.getByText('Updated Name')).toBeInTheDocument();
      expect(screen.queryByText('💾 Save')).not.toBeInTheDocument();
    });
  });

  it('should handle errors gracefully', async () => {
    const errorMocks = [
      {
        request: {
          query: GET_USER,
          variables: { id: '999' },
        },
        error: new Error('User not found'),
      },
    ];

    render(
      <MockedProvider mocks={errorMocks} addTypename={false}>
        <UserProfile userId="999" />
      </MockedProvider>
    );

    await waitFor(() => {
      expect(screen.getByText(/❌ Error: User not found/)).toBeInTheDocument();
    });
  });
});

// 🎯 Integration tests
describe('GraphQL Integration Tests', () => {
  let server: ApolloServer;
  let url: string;

  beforeAll(async () => {
    server = await createEnhancedServer();
    await server.start();
    
    const { url: serverUrl } = await server.listen({ port: 0 });
    url = serverUrl;
  });

  afterAll(async () => {
    await server.stop();
  });

  it('should handle complex nested queries', async () => {
    const query = `
      query ComplexQuery {
        users {
          id
          name
          posts {
            id
            title
            author {
              id
              name
            }
          }
        }
      }
    `;

    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query }),
    });

    const result = await response.json();

    expect(result.errors).toBeUndefined();
    expect(result.data.users).toBeDefined();
    expect(Array.isArray(result.data.users)).toBe(true);
  });

  it('should handle mutations with validation', async () => {
    const mutation = `
      mutation TestMutation($input: CreateUserInput!) {
        createUser(input: $input) {
          id
          name
          email
        }
      }
    `;

    const variables = {
      input: {
        name: 'Integration Test User',
        email: '[email protected]',
        age: 28,
      },
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: mutation, variables }),
    });

    const result = await response.json();

    expect(result.errors).toBeUndefined();
    expect(result.data.createUser).toMatchObject({
      name: 'Integration Test User',
      email: '[email protected]',
    });
    expect(result.data.createUser.id).toBeTruthy();
  });
});

🎯 Conclusion

Congratulations! 🎉 You’ve mastered GraphQL with TypeScript and built enterprise-ready, type-safe APIs! You now have the expertise to:

  • 🔥 Master GraphQL schemas with complete TypeScript type safety and validation
  • 🏗️ Build scalable resolvers with efficient data loading and caching patterns
  • 🎯 Create type-safe clients with automatic code generation and Apollo integration
  • 🧪 Test GraphQL systems comprehensively from schema to client components

GraphQL combined with TypeScript provides the ultimate API development experience - from strongly typed schemas to automatic client code generation. You’re now equipped to build APIs that scale from simple applications to complex enterprise systems while maintaining complete type safety throughout the stack.

Keep exploring advanced patterns like subscriptions, schema stitching, and federation. Remember that great GraphQL APIs are not just about the technology - they’re about creating amazing developer experiences that make building applications a joy! 🚀✨